With the SDK
In the previous exercise, we originated a simple asset, ACME Corp equity. As we did so, we discovered that there are many more configurable properties available to address common business requirements.
The Polymesh Dashboard is constructed with the SDK. The SDK supports every process you see there, and more. Use the SDK to build integrations with internal systems. Fortunately, the SDK's methods are intelligible when you know what it is you intend to do.
If you want to take a closer look at the SDK, a peek into the SDK documentation is recommendable.
The SDK is a comprehensive set of business-level methods for inspecting and interacting with the Polymesh network using either Javascript or Typescript, at the developer's discretion.
You can find it here @polymeshassociation/polymesh-sdk
Preconditions
If you went through the Quick Start, we can assume that you have created an account (a public-private key pair) on the Polymesh Testnet, that you associated it with an account, and that you credited it with POLYX. We shall call this personal signing key aliceKey
, and the personal account it represents alice
.
Here, we are going to follow along the Token Studio Dashboard exercise, and do the same mistake. whereby she creates the asset with her personal account, and which we will eventually fix. The credible simple reason why Alice created the asset with her personal account is that her and her co-founders wanted to act fast so as to have the ticker available before getting the company through CDD.
To recap:
- Alice, ACME's CEO and acting agent, already has an individual Polymesh account, named
alice
, tied to a primary private key namedaliceKey
; aliceKey
's private key is based on the"word1 word2 ..."
mnemonic;- A Polymesh client has been instantiated by Alice so she can do the next actions:
- TypeScript
- JavaScript
const signingManagerAlice: LocalSigningManager =
await LocalSigningManager.create({
accounts: [
{
mnemonic: 'word1 word2 ...',
},
],
});
const apiAlice: Polymesh = await Polymesh.connect({
nodeUrl: 'wss://testnet-rpc.polymesh.live', // or your preferred node
signingManager: signingManagerAlice,
});
const signingManagerAlice = await LocalSigningManager.create({
accounts: [
{
mnemonic: 'word1 word2 ...',
},
],
});
const apiAlice = await Polymesh.connect({
nodeUrl: 'wss://testnet-rpc.polymesh.live', // or your preferred node
signingManager: signingManagerAlice,
});
Ticker reservation
Before creating the asset proper, Alice needs to reserve the ACME ticker so that it is not squatted while the founders incorporate the company. Think of it on the same level as grabbing your .com
domain as early as possible:
- TypeScript
- JavaScript
const reservationQueue: TransactionQueue<TickerReservation> =
await apiAlice.assets.reserveTicker({
ticker: 'ACME',
});
const reservationQueue = await apiAlice.assets.reserveTicker({
ticker: 'ACME',
});
The TransactionQueue
type is just that, a queue. The transaction(s) in it have not been launched. Notice that:
- It is a generic type parameterised with
TickerReservation
. This means that, eventually, the queue yields an instance ofTickerReservation
; - The constructor of
TransactionQueue
expects acontext
; it is in thiscontext
object thatapiAlice
is referenced so it is understood thataliceKey
is the private key to use for signing; - Each transaction in the queue has its own status;
- The queue itself has its own status.
Of note, the reservation cost, at the time of writing, is of 1,000 POLYX, before network fees.
Let's run it.
- TypeScript
- JavaScript
const reservation: TickerReservation = await reservationQueue.run();
const reservation = await reservationQueue.run();
It is at this point that the necessary signatures are collected for the transactions. apiAlice
was created straight with the mnemonic, so the signature will be affixed automatically. However, if this was taking place in the browser, for instance in the Token Studio Dashboard and with the Polymesh Wallet extension, then there is a possibility that Alice will refuse to sign when prompted.
You would need to try .run() catch
it for errors. Here, we opted for clarity and omitted this detail.
Also note that with await reservationQueue.run()
we patiently wait for the queue to finalise all its transactions. However, a TransactionQueue
can provide intermediate information about its changing status and that of its component transactions. If this is of interest to you, you can pass callbacks to onStatusChange
and onTransactionStatusChange
.
At this stage, Alice owns the reservation. You can confirm it with the following:
- TypeScript
- JavaScript
const alice: Identity = await apiAlice.getSigningIdentity();
const details: TickerReservationDetails = await reservation.details();
const owner: Identity = details.owner;
assert(owner.did === alice.did);
const alice = await apiAlice.getSigningIdentity();
const { owner } = await reservation.details();
assert(owner.did === alice.did);
Something is not immediately apparent from the few lines of code above. It is nonetheless important to point it out.
When we created apiAlice
with await Polymesh.connect()
, we passed a signingManager
which was created with an accountMnemonic
that allowed it to recreate the aliceKey
private key. On chain the public key is expressed in SS58 format and referred to as an "address" or "account". The account is then is associated with an identity, whether as a primary key, like here, or a secondary one. When the private key is used to sign a transaction, it is the associated identity that will be considered to be the one doing the action.
This associated account information is not embedded in the private key. It is an association that is stored on the blockchain, may change in the future, and as such, it needs to be retrieved to be known.
So when we wrote await
in await apiAlice.getSigningIdentity()
, this is no accident. We indeed need to do a round trip to the blockchain to know what account our public key is associated with.
Ticker creation
Now that the ticker is reserved, it is time to issue the asset.
Oh wait! The reservation may have happened some time ago. After all a reservation remains valid for 60 days, for instance. And your const reservation: TickerReservation
instance might no longer be in memory.
How do you get it back? Use assets.getTickerReservation
:
- TypeScript
- JavaScript
const signingManagerAlice: LocalSigningManager = await LocalSigningManager.create({...});
const apiAlice: Polymesh = await Polymesh.connect({...});
const reservation: TickerReservation = await apiAlice.assets.getTickerReservation({
"ticker": "ACME"
});
const signingManagerAlice = await LocalSigningManager.create({...});
const apiAlice = await Polymesh.connect({...});
const reservation = await apiAlice.assets.getTickerReservation({
"ticker": "ACME"
});
You will note that the class TickerReservation
, just like the transaction queue, keeps a protected context: Context
. This context
in turn keeps a polymeshApi
. It is in there that we find the implicit knowledge that it is alice
's account that is asking for the next actions. If it were any other account that had called assets.getTickerReservation
, this other account would not be able to follow up with a .createAsset
command because it doesn't own the reservation.
With our reservation
in memory we now can create it:
- TypeScript
- JavaScript
const assetQueue: TransactionQueue<Asset> = await reservation.createAsset({
name: 'ACME Co',
assetType: KnownAssetType.EquityPreferred,
isDivisible: false,
});
const assetQueue = await reservation.createAsset({
name: 'ACME Co',
assetType: 'EquityPreferred',
isDivisible: false,
});
We have another queue, so, as we did before:
- TypeScript
- JavaScript
const asset: Asset = await assetQueue.run();
const asset = await assetQueue.run();
Implicit in the creation of this asset is that Alice, as a private individual, is both the asset's owner and its primary issuance agent (PIA). We were satisfied with this situation only up to this point. Now this needs to change.
This asset
instance will not always be in memory, so if we wanted to fetch it at a later date, we would do:
- TypeScript
- JavaScript
const asset: Asset = await apiAlice.assets.getAsset({
ticker: 'ACME',
});
const asset = await apiAlice.assets.getAsset({
ticker: 'ACME',
});
Secondary Accounts
Now we assume that ACME has gone through CDD and has an account, complete with a private key and its mnemonic, which, in a mirror fashion of that of Alice gives us:
- TypeScript
- JavaScript
const signingManagerAcme: LocalSigningManager =
await LocalSigningManager.create({
accounts: [
{
mnemonic: 'word21 word22 ...',
},
],
});
const apiAcme: Polymesh = await Polymesh.connect({
nodeUrl: 'wss://testnet-rpc.polymesh.live', // or your preferred node
signingManager: signingManagerAcme,
});
const acme: Identity = await apiAcme.getSigningIdentity();
const acmeDid: string = acme.did;
const signingManagerAcme = await LocalSigningManager.create({
accounts: [
{
mnemonic: 'word21 word22 ...',
},
],
});
const apiAcme = await Polymesh.connect({
nodeUrl: 'wss://testnet-rpc.polymesh.live', // or your preferred node
signingManager: signingManagerAcme,
});
const acme = await apiAcme.getSigningIdentity();
const acmeDid = acme.did;
On her end, Alice, has created another mnemonic for a private key she intends to use when she acts as the CEO of ACME. Again:
- TypeScript
- JavaScript
const signingManagerCeo: LocalSigningManager = await LocalSigningManager.create(
{
accounts: [
{
mnemonic: 'word31 word32 ...',
},
],
}
);
const apiCeo: Polymesh = await Polymesh.connect({
nodeUrl: 'wss://testnet-rpc.polymesh.live', // or your preferred node
signingManager: signingManagerCeo,
});
const signingManagerCeo = await LocalSigningManager.create({
accounts: [
{
mnemonic: 'word31 word32 ...',
},
],
});
const apiCeo = await Polymesh.connect({
nodeUrl: 'wss://testnet-rpc.polymesh.live', // or your preferred node
signingManager: signingManagerCeo,
});
At this point, apiCeo
has no associated account. It is a signing key in search of an account. Alice first needs to get her public key:
- TypeScript
- JavaScript
const pubCeo: string = apiCeo.accountManagement.getAccount().address;
const pubCeo = apiCeo.accountManagement.getAccount().address;
Then she needs to send this pubCeo
information to apiAcme
. When this is done, the company can invite her to be a secondary key:
- TypeScript
- JavaScript
await apiAcme.accountManagement.inviteAccount({
targetAccount: pubCeo,
});
await apiAcme.accountManagement.inviteAccount({
targetAccount: pubCeo,
});
With the invitation sent out into the blockchain, back at her computer, Alice, with knowledge of ACME account's number, acmeDid
, can do:
- TypeScript
- JavaScript
const ceoAccount: Account = apiCeo.accountManagement.getAccount();
const pendingAuthorizations: AuthorizationRequest[] =
await ceoAccount.authorizations.getReceived();
const acmeAuthorization: AuthorizationRequest = pendingAuthorizations.find(
(pendingAuthorization: AuthorizationRequest) => {
return pendingAuthorization.issuer.did === acmeDid;
}
);
const acceptQueue: TransactionQueue<void> = await acmeAuthorization.accept();
await acceptQueue.run();
const ceoAccount = apiCeo.getAccount();
const pendingAuthorizations = await ceoAccount.authorizations.getReceived();
const acmeAuthorization = pendingAuthorizations.find((pendingAuthorization) => {
pendingAuthorization.issuer.did === acmeDid;
});
const acceptQueue = await acmeAuthorization.accept();
await acceptQueue.run();
With this done, apiCeo
now allows Alice to properly act as the CEO, on behalf of ACME.
Asset ownership transfer
With the keys and accounts finally set right, it is time for Alice to fix the asset situation, and transfer its ownership to ACME.
Since it is Alice the individual who owns the asset, she has to go back to using her personal account.
- TypeScript
- JavaScript
const asset: Asset = await apiAlice.assets.getAsset({
ticker: 'ACME',
});
const transferQueue: TransactionQueue<Asset> = await asset.transferOwnership({
target: acmeDid,
});
await transferQueue.run();
const asset = await apiAlice.assets.getAsset({
ticker: 'ACME',
});
const transferQueue = await asset.transferOwnership({
target: acmeDid,
});
await transferQueue.run();
With the authorisation recorded in the blockchain, and on the way, Alice can stay on the same computer and switch from her personal identity to her identity as the CEO of ACME to accept the authorisation.
She first needs to recall her personal account number, or did
.
- TypeScript
- JavaScript
const alice: Identity = await apiAlice.getSigningIdentity();
const aliceDid: string = alice.did;
const alice = await apiAlice.getSigningIdentity();
const aliceDid string = alice.did;
So she can narrow down the authorisation, instead of blindly accepting whatever is in the pipeline.
- TypeScript
- JavaScript
const pendingAuthorizations: AuthorizationRequest[] =
await ceoAccount.authorizations.getReceived();
const transferAuthorization: AuthorizationRequest = pendingAuthorizations.find(
(pendingAuthorization: AuthorizationRequest) => {
return pendingAuthorization.issuer.did === aliceDid;
}
);
const acceptQueue: TransactionQueue<void> =
await transferAuthorization.accept();
await acceptQueue.run();
const pendingAuthorizations = await ceoAccount.authorizations.getReceived();
const transferAuthorization = pendingAuthorizations.find(
(pendingAuthorization) => {
return pendingAuthorization.issuer.did === aliceDid;
}
);
const acceptQueue = await transferAuthorization.accept();
await acceptQueue.run();
With this, the asset is rightfully owned by ACME the company.
Compliance
We are not done yet with the asset, though.
As the CEO, Alice still needs to do one more step, that is, to define the conditions of ownership. Namely, require any account who acquires the asset to not have a jurisdictional attestation of Liechtenstein. An exception will be made for the primary issuance agent, who is simply used as a conduit and can send to anyone.
We use ACME's account as the KYC service provider, but realistically, it should be another account.
- TypeScript
- JavaScript
const acmeCompliance: Compliance = asset.compliance;
const acmeRequirements: Requirements = acmeCompliance.requirements;
const acme: Identity = await apiCeo.getSigningIdentity();
const setRequirementsQueue: TransactionQueue<Asset> =
await acmeRequirements.set({
requirements: [
[
{
target: ConditionTarget.Sender,
type: ConditionType.IsExternalAgent,
},
],
[
{
target: ConditionTarget.Receiver,
type: ConditionType.IsPresent,
claim: {
type: ClaimType.KnowYourCustomer,
scope: {
type: ScopeType.Ticker,
value: asset.ticker,
},
},
trustedClaimIssuers: [
{
identity: acme.did,
trustedFor: [ClaimType.KnowYourCustomer],
},
],
},
{
target: ConditionTarget.Receiver,
type: ConditionType.IsAbsent,
claim: {
type: ClaimType.Jurisdiction,
code: CountryCode.Li,
scope: {
type: ScopeType.Ticker,
value: asset.ticker,
},
},
trustedClaimIssuers: [
{
identity: acme.did,
trustedFor: [ClaimType.Jurisdiction],
},
],
},
],
],
});
const updatedAsset: Asset = await setRequirementsQueue.run();
const acmeCompliance = asset.compliance;
const acmeRequirements = acmeCompliance.requirements;
const acme = await apiCeo.getSigningIdentity();
const setRequirementsQueue = await acmeRequirements.set({
requirements: [
[
{
target: 'Sender',
type: 'IsExternalAgent',
},
],
[
{
target: 'Receiver',
type: 'IsPresent',
claim: {
type: 'KnowYourCustomer',
scope: {
type: 'Ticker',
value: asset.ticker,
},
},
trustedClaimIssuers: [
{
identity: acme.did,
trustedFor: ['KnowYourCustomer'],
},
],
},
{
target: 'Receiver',
type: 'IsAbsent',
claim: {
type: 'Jurisdiction',
code: 'Li',
scope: {
type: 'Ticker',
value: asset.ticker,
},
},
trustedClaimIssuers: [
{
identity: acme.did,
trustedFor: ['Jurisdiction'],
},
],
},
],
],
});
const updatedAsset: Asset = await setRequirementsQueue.run();
With this, the asset is originated. Nobody, including Alice under her personal account, is yet a holder of any amount of the asset, though, we remedy that in the next chapter when we tackle distribution.