Skip to content

Commit

Permalink
feat: add MultiSig examples
Browse files Browse the repository at this point in the history
add examples for creating and using MultiSig accounts
  • Loading branch information
polymath-eric committed Jul 11, 2024
1 parent 41f7646 commit 3b3f390
Show file tree
Hide file tree
Showing 4 changed files with 555 additions and 402 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
},
"dependencies": {
"@polymeshassociation/local-signing-manager": "^3.1.0",
"@polymeshassociation/polymesh-sdk": "23.0.0",
"@polymeshassociation/polymesh-sdk": "24.7.0-alpha.6",
"bluebird": "^3.7.2",
"dotenv": "^16.0.3"
},
Expand Down
19 changes: 15 additions & 4 deletions src/common/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,21 @@ let api: Polymesh;
/**
* @hidden
*/
export async function getClient(mnemonic?: string): Promise<Polymesh> {
const localSigningManager = await LocalSigningManager.create({
accounts: [mnemonic ? { mnemonic } : { uri: '//Alice' }],
});
export async function getClient(mnemonics?: string | string[]): Promise<Polymesh> {
let localSigningManager: LocalSigningManager;
if (!mnemonics) {
localSigningManager = await LocalSigningManager.create({
accounts: [{ uri: '//Alice' }],
});
} else if (typeof mnemonics === 'string') {
localSigningManager = await LocalSigningManager.create({
accounts: [{ mnemonic: mnemonics }],
});
} else {
localSigningManager = await LocalSigningManager.create({
accounts: mnemonics.map(mnemonic => ({ mnemonic })),
});
}

if (!api) {
api = await Polymesh.connect({
Expand Down
150 changes: 150 additions & 0 deletions src/examples/multiSig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { BigNumber } from '@polymeshassociation/polymesh-sdk';

import { getClient } from '~/common/client';

/*
This script showcases MultiSig related functionality. It:
- Demonstrates signing with different accounts using derivation paths
- Creates a MultiSig
- Accept MultiSig authorizations
- Joins the MultiSig to an Identity
- Signing transactions with different accounts
*/
(async (): Promise<void> => {
console.log('\n----------------------------------------------------------------------\n');
console.log('💡 Creating and using a MultiSig account:\n');
const accountSeed = '//Alice';

// Note: addresses assume `//Alice` as base
const creatorAddress = '5Ge4F7x6oVYQtNzyK2Y9tVWeeRoNMeWU8Rh5LbQ8A7KzpzGt';
const signerOne = '5EeaL92sHzBXti4oDQJLKbHjAAJzzHLVBgYK7pG9KpRKtXHd';
const signerTwo = '5DRygDgq77PN6z9sBkjhDEAoXtWNpSzn2F8yWY3CzUuWAFV6';

console.log('🔌 Connecting to the node...\n');
const api = await getClient([
accountSeed,
`${accountSeed}/creator`, // 5Ge4F7x6oVYQtNzyK2Y9tVWeeRoNMeWU8Rh5LbQ8A7KzpzGt (if seed is //Alice)
`${accountSeed}/signerOne`, // 5EeaL92sHzBXti4oDQJLKbHjAAJzzHLVBgYK7pG9KpRKtXHd (if seed is //Alice)
`${accountSeed}/signerTwo`, // 5DRygDgq77PN6z9sBkjhDEAoXtWNpSzn2F8yWY3CzUuWAFV6 (if seed is //Alice)
]);

// The creator of the MultiSig has to go through a KYC process
const registerCreator = await api.identities.registerIdentity({
targetAccount: creatorAddress,
createCdd: true,
});
const creatorId = await registerCreator.run();

// Transfer POLYX to the creator. Note: The creator's primary key is responsible for all of the MultiSig's fees
const transfer = await api.network.transferPolyx({
to: creatorAddress,
amount: new BigNumber(5000),
});
await transfer.run();
console.log(
`🚀 Creator account DID ("${creatorId.did}") created and account was seeded with POLYX\n`
);

const [signerOneAccount, signerTwoAccount] = await Promise.all([
api.accountManagement.getAccount({ address: signerOne }),
api.accountManagement.getAccount({ address: signerTwo }),
]);

const createMultiSig = await api.accountManagement.createMultiSigAccount(
{
signers: [signerOneAccount, signerTwoAccount],
requiredSignatures: new BigNumber(2),
},
{
signingAccount: creatorAddress,
}
);
const multiSig = await createMultiSig.run();

console.log(`ℹ️ multiSig created. Address: "${multiSig.address}"\n`);

// Each signer has to accept joining the MultiSig, here we perform both actions in parallel
const [signerOneAuths, signerTwoAuths] = await Promise.all([
signerOneAccount.authorizations.getReceived(),
signerTwoAccount.authorizations.getReceived(),
]);

const [signerOneAccept, signerTwoAccept] = await Promise.all([
signerOneAuths[0].accept({ signingAccount: signerOne }),
signerTwoAuths[0].accept({ signingAccount: signerTwo }),
]);

await Promise.all([await signerOneAccept.run(), await signerTwoAccept.run()]);
console.log('👥 Signers accepted the MultiSig authorization\n');
/**
* Before the MultiSig can perform actions it needs to be associated with a CDD claim
*
* Here we use a convenience method to attach the MultiSig directly to the creator account.
* To attach to a different DID the MultiSig can be treated like any other account and be
* targeted with `api.accountManagement.inviteAccount`.
*
* The creator's primary key is always responsible for the MultiSig's fees. If the multiSig
* joins as a primary key, it will be responsible for its own fees, otherwise the primary
* key will need to maintain adequate POLYX balance to ensure extrinsics are able to be
* submitted.
*
* Here the MultiSig joins the creator's identity as a secondary key with full permissions
*
*/
const joinCreator = await multiSig.joinCreator(
{ asPrimary: false, permissions: { assets: null, transactions: null, portfolios: null } },
{ signingAccount: creatorAddress }
);
await joinCreator.run();
console.log('🔗 MultiSig joined creator as a secondary key\n');

/**
* Submitting transactions for a MultiSig is a bit different compared to regular accounts.
*
* Because the signing account is a multiSig signer, the transaction needs to be wrapped as
* a proposal, which will then need to be accepted. `runAsProposal` must be called instead of
* the usual `.run`.
*
* Generically the procedure's `.multiSig` param can be checked, if set the `runAsProposal`
* should be called
*/
const createPortfolio = await api.identities.createPortfolio(
{
name: 'MultiSig Portfolio',
},
{ signingAccount: signerOne }
);

console.log('📝 submitting proposal for MultiSig', createPortfolio.multiSig?.address); // Since `.multiSig` is set, `runAsProposal` should be used
const portfolioProposal = await createPortfolio.runAsProposal();
console.log(`ℹ️ proposal created id: ${portfolioProposal.id.toString()}\n`);

/**
* Accepting and rejecting proposal transactions will not be wrapped even though the
* signer is a MultiSig signer the transaction will not be wrapped as a proposal.
*/
const acceptProposal = await portfolioProposal.approve({ signingAccount: signerTwo });
// acceptProposal.multiSig - will be null, since approve is not wrapped as a proposal
await acceptProposal.run();
console.log(`✅ Account: ${signerTwo} approved proposal ${portfolioProposal.id.toString()}\n`);

const reserveTicker = await api.assets.reserveTicker(
{
ticker: 'MULTI',
},
{ signingAccount: signerOne }
);

const reserveProposal = await reserveTicker.runAsProposal();
console.log(`ℹ️ proposal created id: ${reserveProposal.id.toString()}\n`);

/**
* Rejecting a proposal is similar to accepting one
*/
const rejectProposal = await reserveProposal.reject({ signingAccount: signerTwo });
await rejectProposal.run();
console.log(`❌ Account: ${signerTwo} rejected proposal: ${reserveProposal.id.toString()}`);

console.log('🔌 Disconnecting from the node...\n');
await api.disconnect();
})();
Loading

0 comments on commit 3b3f390

Please sign in to comment.