Skip to main content

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:

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 except DELEGATECALL.

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.

Uploading an LSP3 metadata automatically
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.

Providing a previously uploaded LSP3 metadata IPFS URL
await lspFactory.UniversalProfile.deploy({
controllerAddresses: ['0x...'],
lsp3Profile: 'ipfs://QmQ7Wq4y2gWiuzB4a4Wd6UiidKNpzCJRpgzFqQwzyq6SsV'
});
};
Providing a previously uploaded LSP3 metadata URL
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:

Providing an already uploaded LSP3 metadata url and JSON file itself
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:

Providing an already uploaded LSP3 metadata url and hash values
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.

Setting LSP3 metadata to be uploaded with profile and background images
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.

Calling uploadMetaData on an LSPFactory instance
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",
...
}
}
}
Calling uploadMetaData on the uninstantiated class
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 to true for ERC725Account and LSP6KeyManager
  • The property deployProxy defaults to false for UniversalReceiverDelegate.

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.

Deploying a Universal Profile using a custom ERC725Account base contract implementation
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.

Using a custom UniversalReceiverDelegate address
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.

Deploying a Universal Profile with all contracts set to version 0.5.0
await lspFactory.UniversalProfile.deploy({...}, {
version: '0.5.0'
});
Deploying a Universal Profile at version 0.5.0, with a KeyManager set to version to 0.4.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.

Deploying a Universal Profile with a custom KeyManager implementation
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.

Passing ipfsGateway URL
lspFactory.UniversalProfile.deploy({...}, {
ipfsGateway: 'https://ipfs.infura.io:5001'
})
Passing ipfsGateway URL string with port set
lspFactory.UniversalProfile.deploy({...}, {
ipfsGateway: 'https://ipfs.infura.io' // No port set. Port 5001 will be used
})
Passing ipfsGateway options as an object
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.

Reactive deployment of a Universal Profile
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: {
...
}
}
}