Deploying a Universal Profile
LSPFactory allows you to quickly deploy and configure a Universal Profile consisting of an LSP0 ERC725 Account, an LSP6 Key Manager, and an LSP1-UniversalReceiver smart contract:
await lspFactory.UniversalProfile.deploy(profileProperties [, options]);
This will deploy the following contracts:
- LSP0 ERC725 Account
- LSP6 Key Manager
- And link to a pre-deployed LSP1 Universal Receiver
After, it will:
- upload metadata to IPFS and set the LSP3 Universal Profile metadata,
- attach the Universal Receiver Delegate to the ERC725 Account,
- set the Key Manager as the owner of the ERC725 Account, and
- set all permissions for the
controllerAddresses
exceptDELEGATECALL
.
These smart contracts linked with some LSP3 Universal Profile Metadata form a Universal Profile. The metadata is the 'face' of your profile and contains information such as your name, description, and profile image.
Profile Properties
Inside the profileProperties
object, you can set profile configuration options such as the controller addresses and LSP3 metadata.
Controller Addresses
You can set the addresses which should be able to control your Universal Profile initially by passing in the controllerAddresses
. The addresses that were passed here will be given all LSP6 KeyManager permissions except DELEGATECALL
to prevent accidental misuse. If your controller keys require DELEGATECALL
, you can change the permission after deployment.
The property controllerAddresses
can be filled with addresses of externally owned accounts (EOAs) or another smart contract that can call the execute(calldata)
function on the KeyManager.
await lspFactory.UniversalProfile.deploy({
controllerAddresses: [
'0x7Ab53a0C861fb955050A8DA109eEeA5E61fd8Aa4',
'0x56fE4E7dc2bc0b6397E4609B07b4293482E3F72B',
],
});
Adding LSP3 Metadata
When deploying a Universal Profile with LSPFactory, you can specify your Universal Profile metadata using the lsp3Profile
key in the profileProperties
object:
await lspFactory.UniversalProfile.deploy({
controllerAddresses: ['0x...'],
lsp3Profile: myUniversalProfileData,
});
Info
Profile Metadata can be passed as either a JSON object containing the LSP3Metadata you want to upload or a URL of your previously uploaded metadata.
If an LSP3MetaData object is passed, LSPFactory will process and upload your metadata to IPFS.
info
See Upload Options for details on how to specify a custom IPFS gateway.
await lspFactory.UniversalProfile.deploy({
controllerAddresses: ['0x...'],
lsp3Profile: {
name: 'My-Cool-Profile',
description: 'My cool Universal Profile',
tags: ['public-profile'],
links: [{
title: 'My Website',
url: 'www.my-website.com'
}],
...
}
});
};
The following two examples will download the JSON file before hashing it and generating the proper JSONURL value.
await lspFactory.UniversalProfile.deploy({
controllerAddresses: ['0x...'],
lsp3Profile: 'ipfs://QmQ7Wq4y2gWiuzB4a4Wd6UiidKNpzCJRpgzFqQwzyq6SsV'
});
};
await lspFactory.UniversalProfile.deploy({
controllerAddresses: ['0x...'],
lsp3Profile: 'https://mycoolserver.com/myProfile.json'
});
};
You can also provide the JSON file yourself to generate the hash value:
await lspFactory.UniversalProfile.deploy({
controllerAddresses: ['0x...'],
lsp3Profile: {
json: lsp3ProfileJson,
url: 'https://mycoolserver.com/myProfile.json',
},
});
Or you can provide the hash value and then uploaded file URL:
await lspFactory.UniversalProfile.deploy({
controllerAddresses: ['0x...'],
lsp3Profile: {
hash: '0xfdafad027ecfe57eb4ad047b938805d1dec209d6e9f960fc320d7b9b11cbed14',
hashFunction: 'keccak256(utf8)',
url: 'https://mycoolserver.com/file.json',
},
});
Setting Images in LSP3MetaData
The properies profileImage
and backgroundImage
can be passed inside the lsp3Profile
object. These can be given as an object containing previously uploaded image Metadata, a Javascript File
object if used client-side.
Pre-uploaded Images
An LSP3 Profile requires the properties profileImage
and backgroundImage
to be uploaded in multiple sizes so that interfaces can choose which one to load for better loading performance.
If you already have an image uploaded to IPFS in multiple sizes, you can pass image metadata inside the lsp3Profile
object when deploying a Profile.
info
Both profileImage
and backgroundImage
take an array, where each element is the metadata of different image size.
const myUniversalProfileData = {
name: 'My Universal Profile',
description: 'My cool Universal Profile',
profileImage: [
{
width: 500,
height: 500,
hashFunction: 'keccak256(bytes)',
hash: '0xfdafad027ecfe57eb4ad044b938805d1dec209d6e9f960fc320d7b9b11cced14', // bytes32 hex string of the image hash
url: 'ipfs://QmPLqMFDxiUgYAom3Zg4SiwoxDaFcZpHXpCmiDzxrajSGp',
},
... // Multiple image sizes should be included
],
backgroundImage: [
{
width: 500,
height: 500,
hashFunction: 'keccak256(bytes)',
hash: '0xfdafad027ecfe57eb4ad047b938805d1dec209d6e9f960fc320d7b9b11cbed14', // bytes32 hex string of the image hash
url: 'ipfs://QmPLqMFHxiUDYAom3Zg4SiwoxDaFcZpHXpAmiDzxrtjSGp',
},
... // Multiple image sizes should be included
],
};
await lspFactory.UniversalProfile.deploy({
controllingAccounts: ['0x...'],
lsp3Profile: myUniversalProfileData
});
};
Using the Javascript File object
Javascript offers a File
object for easy handling of files inside a browser. Developers can pass these to profileImage
and backgroundImage
fields to allow easy drag and drop of images from a user interface.
caution
Javascript's File
object is only available when using javascript in the browser. If using LSPFactory in a Node environment, image metadata should be passed.
<input type="file" id="input">
<script>
const myLocalFile = document.getElementById('input').files[0];
const myUniversalProfileData = {
name: "My Universal Profile",
description: "My cool Universal Profile",
profileImage: myLocalFile,
backgroundImage: myLocalFile,
tags: ['Fashion', 'Design'],
links: [{
title: "My Website",
url: "www.my-website.com"
}],
};
await lspFactory.UniversalProfile.deploy({
controllingAccounts: ['0x...'],
lsp3Profile: myUniversalProfileData
});
};
<script/>
LSPFactory will create five resized versions of the passed image, with max sizes of 1800x1800
, 1024x1024
, 640x640
, 320x320
, 180x180
. These resized images will be set inside the LSP3Metadata
and attached to the ERC725Account
.
Uploading LSP3 metadata to IPFS
You can upload your LSP3 metadata before deploying a Universal Profile using the uploadMetaData()
method. The function uses the same lsp3Profile
object schema defined above when deploying a Universal Profile. Returns an object containing the IPFS upload location of your metadata and your lsp3Metdata
as a javascript object.
await myLSPFactory.UniversalProfile.uploadMetaData(lsp3Profile [, options]);
To upload using a custom IPFS gateway, pass the options
object. The field is the same options
object used when deploying a Universal Profile. Read more.
The uploadMetaData()
function is available as a static or non-static method to be called without instantiating an LSPFactory
object.
await myLSPFactory.UniversalProfile.uploadMetaData(myLSP3MetaData);
> {
hash: '0x1234...',
hashFunction: 'keccak256(utf8)',
url: 'https://ipfs.lukso.network/ipfs/QmPzUfdKhY6vfcLNDnitwKanpm5GqjYSmw9todNVmi4bqy',
json: {
LSP3Profile: {
name: "My Universal Profile",
description: "My Cool Universal Profile",
...
}
}
}
await UniversalProfile.uploadMetaData(myLSP3MetaData);
> // same as above
Deployment Configuration
A Universal Profile is composed of three smart contracts. LSP0 ERC725 Account, LSP6 Key Manager, and LSP1-UniversalReceiver.
When deploying a Universal Profile, you can configure how developers should deploy these contracts inside the contractDeploymentOptions
object. Builders can configure each contract separately. The available options are the same for all contracts.
Under the version
key developers can pass a version number, custom bytecode or a base contract address to be used during deployment.
await lspFactory.UniversalProfile.deploy({...}, {
ERC725Account: {
version: '0.4.1', // Version number
},
UniversalReceiverDelegate: {
version: '0x...' // Custom bytecode
},
KeyManager: {
version: '0x6c1F3Ed2F99054C88897e2f32187ef15c62dC560' // Base contract address
}
})
Proxy Deployment
Proxy deployment allows you to determine whether your contract should be deployed as a minimal proxy contract based on EIP1167 or an entire contract with a constructor.
lspFactory.UniversalProfile.deploy({...}, {
ERC725Account: {
deployProxy: false,
},
})
A proxy contract is a lightweight contract that inherits its logic by referencing the address of a contract already deployed on the blockchain. Inheriting allows cheaper deployment of Universal Profiles because only the proxy contract needs to be deployed.
LSPFactory stores base contract addresses inside the version file.
info
The function will use the latest available base contract version if no version is specified in the version parameter. LSPFactory stores base contract addresses for different versions internally.
info
- The property
deployProxy
defaults totrue
forERC725Account
andLSP6KeyManager
- The property
deployProxy
defaults tofalse
forUniversalReceiverDelegate
.
When using proxy deployment, LSPFactory will check that there is some bytecode deployed at the base contract address before deploying. A new base contract will be deployed and referenced in the proxy contract if there is none. This process is helpful when using LSPFactory on a local development network like Hardhat, where there will be no pre-deployed base contracts.
Universal Receiver Delegate Proxy Deployment
The UniversalReceiverDelegate
is a logic contract that writes to the Universal Profile when it receives some asset. This operation is not specific to any particular Universal Profile, so developers can use the same UniversalReceiverDelegate
contract for multiple different Universal Profile deployments.
By default, LSPFactory will use the latest available version of the UniversalReceiverDelegate
version stored in the version file. This address is used directly on the Universal Profile and is given the SETDATA
LSP6 permission.
Reusing the UniversalReceiverDelegate
address means that no UniversalReceiverDelegate
contract needs to be deployed when deploying a Universal Profile which further reduces the gas cost of Universal Profile deployment.
To specify that your UniversalReceiverDelegate
contract should use proxy deployment, set the property deployProxy
to true
. If no base contract address is specified in the version
parameter a new UniversalReceiverDelegate
base contract will be deployed.
lspFactory.UniversalProfile.deploy({...}, {
UniversalReceiverDelegate: {
deployProxy: true,
version: '0x00b1d454Eb5d917253FD6cb4D5560dEC30b0960c',
},
})
Using a Custom Address
When using proxy deployment you can specify the base contract address by passing the version
parameter. This allows you to deploy a specific contract implementation by using a custom base contract you have deployed.
Any base contract address that developers pass here must adhere to the relevant LSP contract standard it is being used for.
lspFactory.UniversalProfile.deploy({...}, {
ERC725Account: {
version: '0x00b1d454Eb5d917253FD6cb4D5560dEC30b0960c',
},
})
info
The UniversalReceiverDelegate
contract does not use proxy deployment by default. If an address is passed to the UniversalReceiverDelegate
version
parameter and deployProxy
is not set to true
, LSPFactory will set the provided address directly on the ERC725Account as the LSP1UniversalReceiverDelegate key and given the SETDATA
LSP6 permission. You can read more in the section above.
lspFactory.UniversalProfile.deploy({...}, {
UniversalReceiverDelegate: {
version: '0x00b1d454Eb5d917253FD6cb4D5560dEC30b0960c',
deployProxy: false
},
})
Contract Versions
LSPFactory stores the addresses of different base contract versions internally. By specifying a version
number, developers can specify which base contract implementation should be used during deployment.
The version
of all three contracts can be set at once by passing the global version parameter. The version can also be set per contract, which will take precedence over the global parameter.
await lspFactory.UniversalProfile.deploy({...}, {
version: '0.5.0'
});
await lspFactory.UniversalProfile.deploy({...}, {
version: '0.5.0',
KeyManager: {
version: '0.4.0'
}
});
Deploying Custom Bytecode
When deploying a Universal Profile, you can use your custom contract implementation by passing the compiled creation bytecode of a contract you have written as the version
parameter. The bytecode
parameter can be the instantiation bytecode of a custom contract implementation you have written according to your use case. The implementation must meet the relevant LSP standard requirements.
note
The custom bytecode will be deployed and used as part of the Universal Profile. Contracts deployed from custom bytecode will not use any proxy contract deployment.
lspFactory.UniversalProfile.deploy({...}, {
KeyManager: {
version: '0x...'
}
});
IPFS Upload Options
You can specify how you want your profile metadata to be uploaded while passing the options object. Here you can set the IPFS gateway where you want the profile's metadata to be uploaded.
note
The procedure takes a URL string or an object as defined by the IPFS-HTTP Client library which is used internally to interact with the specified IPFS node.
If a URL is passed and no port is specified, the standard 5001 port will be used.
lspFactory.UniversalProfile.deploy({...}, {
ipfsGateway: 'https://ipfs.infura.io:5001'
})
lspFactory.UniversalProfile.deploy({...}, {
ipfsGateway: 'https://ipfs.infura.io' // No port set. Port 5001 will be used
})
lspFactory.UniversalProfile.deploy({...}, {
ipfsGateway: {
host: 'ipfs.infura.io',
port: 5001,
protocol: 'https',
}
})
If the ipfsGateway
parameter is provided, it will override the ipfsGateway
object passed during the instantiation of the LSPFactory for this function call only.
Reactive Deployment
LSPFactory uses RxJS to deploy smart contracts. This can be leveraged for reactive deployment of Universal Profiles. Read more here.
When deployReactive
is set to true
, an RxJS Observable will be returned which will emit events as the deployment progresses.
const observable = await lspFactory.UniversalProfile.deploy({...}, {
deployReactive: true
});
observable.subscribe();
The following events will be emitted:
{
type: 'PROXY_DEPLOYMENT',
contractName: 'ERC725Account',
status: 'PENDING',
transaction: {
...
}
}
{
type: 'PROXY_DEPLOYMENT',
contractName: 'ERC725Account',
status: 'COMPLETE',
contractAddress: '0xa7b2ab323cD2504689637A0b503262A337ab87d6',
receipt: {
...
}
}
{
type: 'TRANSACTION',
contractName: 'ERC725Account',
functionName: 'initialize(address)',
status: 'PENDING',
transaction: {
...
}
}
{
type: 'TRANSACTION',
contractName: 'ERC725Account',
functionName: 'initialize(address)',
status: 'COMPLETE',
receipt: {
...
}
}
{
type: 'PROXY_DEPLOYMENT',
contractName: 'KeyManager',
status: 'PENDING',
transaction: {
...
}
}
{
type: 'PROXY_DEPLOYMENT',
contractName: 'KeyManager',
status: 'COMPLETE',
contractAddress: '0x8fE3f0fd1bc2aCDA6cf3712Cd9C7858B8195DC8E',
receipt: {
...
}
}
{
type: 'TRANSACTION',
contractName: 'KeyManager',
functionName: 'initialize(address)',
status: 'PENDING',
transaction: {
...
}
}
{
type: 'TRANSACTION',
contractName: 'KeyManager',
functionName: 'initialize(address)',
status: 'COMPLETE',
receipt: {
...
}
}
{
type: 'TRANSACTION',
contractName: 'ERC725Account',
functionName: 'setData(bytes32[],bytes[])',
status: 'PENDING',
transaction: {
...
}
}
{
type: 'TRANSACTION',
contractName: 'ERC725Account',
functionName: 'setData(bytes32[],bytes[])',
status: 'COMPLETE',
receipt: {
...
}
}
{
type: 'TRANSACTION',
status: 'PENDING',
contractName: 'ERC725Account',
functionName: 'transferOwnership(address)',
transaction: {
...
}
}
{
type: 'TRANSACTION',
contractName: 'ERC725Account',
functionName: 'transferOwnership(address)',
status: 'COMPLETE',
receipt: {
...
}
}
{
ERC725Account: {
address: '0xa7b2ab323cD2504689637A0b503262A337ab87d6',
receipt: {
...
}
},
KeyManager: {
address: '0x8fE3f0fd1bc2aCDA6cf3712Cd9C7858B8195DC8E',
receipt: {
...
}
}
}