-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Futurepass multisig wallet example (#44)
- Loading branch information
1 parent
87ddbde
commit 46e4965
Showing
5 changed files
with
304 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Futurepass Pallet | ||
|
||
[![Run in StackBlitz](https://img.shields.io/badge/Open_in_StackBlitz-1269D3?style=for-the-badge&logo=stackblitz&logoColor=white)](https://stackblitz.com/github/futureversecom/trn-examples?file=examples%2Fsubstrate%2Fuse-futurepass%2FREADME.md&title=Futurepass%20Pallet%20Examples) [![Pallet Documentation](https://img.shields.io/badge/Pallet_Documentation-black?style=for-the-badge&logo=googledocs&logoColor=white)](https://docs-beta.therootnetwork.com/buidl/substrate/pallet-futurepass) | ||
|
||
> [!IMPORTANT] | ||
> Ensure the following ENV vars are available before running the examples | ||
> | ||
> - `CALLER_PRIVATE_KEY` - Private key of an account that submits the transaction. Follow this guide to [create and fund an account with some test tokens](../../GUIDES.md) on Porcini (testnet) if you don't have one yet. | ||
## Examples | ||
|
||
```bash | ||
# change your working directory to this example first | ||
cd examples/substrate/use-futurepass-multisig | ||
|
||
# export all required environments | ||
export THRESHOLD=2 WALLET_NAME=test SIGNATORIES=0xFFfFffFF000000000000000000000000000003CD,0x25451A4de12dcCc2D166922fA938E900fCc4ED24,0x6D1eFDE1BbF146EF88c360AF255D9d54A5D39408 | ||
|
||
# creates new multisig wallet and note the wallet address | ||
pnpm call:createMultisigWallet | ||
|
||
# export all required environments | ||
export CALLER_PRIVATE_KEY=0xcb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854 // private key of fpass holder | ||
export THRESHOLD=2 SIGNATORIES=0x25451A4de12dcCc2D166922fA938E900fCc4ED24,0x6D1eFDE1BbF146EF88c360AF255D9d54A5D39408 | ||
# call an extrinsic as FPass account with multisig call (system.remarkWithEvent) | ||
pnpm call:proxyExtrinsic | ||
|
||
export CALLER_PRIVATE_KEY=0x79c3b7fc0b7697b9414cb87adcb37317d1cab32818ae18c0e97ad76395d1fdcf // private key of other multisig wallet holder | ||
export THRESHOLD=2 MULTISIG_WALLET=0xe944FAd69B79125706D2481f58b66fcDbED358d7 SIGNATORIES=0x25451A4de12dcCc2D166922fA938E900fCc4ED24,0x6D1eFDE1BbF146EF88c360AF255D9d54A5D39408 | ||
# sign by second account of multisig wallet | ||
pnpm call:signByOtherWallet | ||
|
||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"scripts": { | ||
"call": "tsx --tsconfig ../../../tsconfig.json", | ||
"call:createMultisigWallet": "pnpm call src/createMultisigWallet.ts", | ||
"call:proxyExtrinsic": "pnpm call src/proxyExtrinsic.ts", | ||
"call:signByOtherWallet": "pnpm call src/signByOtherWallet.ts" | ||
} | ||
} |
54 changes: 54 additions & 0 deletions
54
examples/substrate/use-futurepass-multisig/src/createMultisigWallet.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { createTestKeyring } from "@polkadot/keyring"; | ||
import { KeyringOptions } from "@polkadot/keyring/types"; | ||
import { bnToBn, objectSpread, u8aSorted } from "@polkadot/util"; | ||
import { createKeyMulti } from "@polkadot/util-crypto"; | ||
import { withChainContext } from "@trne/utils/withChainContext"; | ||
import { cleanEnv, str } from "envalid"; | ||
|
||
const { CHAIN_ENDPOINT, SIGNATORIES, WALLET_NAME, THRESHOLD } = cleanEnv(process.env, { | ||
CHAIN_ENDPOINT: str({ default: "porcini" }), | ||
SIGNATORIES: str(), // Comma separated signatories | ||
WALLET_NAME: str(), // private key of extrinsic caller | ||
THRESHOLD: str(), | ||
}); | ||
|
||
/** | ||
* Create multisig wallet with the given signatories and threshold | ||
*/ | ||
withChainContext(CHAIN_ENDPOINT, async (api, logger) => { | ||
const genesisHash = api.genesisHash; | ||
const signatoryList = SIGNATORIES.split(","); | ||
console.log("signatoryList::", signatoryList); | ||
const ss58Format = 193; | ||
const options = { | ||
ss58Format, | ||
type: "ethereum", | ||
}; | ||
const keyring = createTestKeyring(options as KeyringOptions, true); | ||
const multiSigOptions = { | ||
genesisHash: genesisHash.toString(), | ||
name: WALLET_NAME.trim(), | ||
tags: [], | ||
}; | ||
const multiSigAddress = addMultisig(signatoryList, THRESHOLD, multiSigOptions, keyring); | ||
|
||
console.log("[1/3] Created multiSig address:", multiSigAddress); | ||
logger.info(multiSigAddress, `Created multiSig address`); | ||
}); | ||
|
||
function addMultisig(addresses, threshold, meta = {}, keyring: KeyringInstance) { | ||
let address = createKeyMulti(addresses, threshold); | ||
address = address.slice(0, 20); // for ethereum addresses | ||
// we could use `sortAddresses`, but rather use internal encode/decode so we are 100% | ||
const who = u8aSorted(addresses.map((who) => keyring.decodeAddress(who))).map((who) => | ||
keyring.encodeAddress(who) | ||
); | ||
const meta1 = objectSpread({}, meta, { | ||
isMultisig: true, | ||
threshold: bnToBn(threshold).toNumber(), | ||
who, | ||
}); | ||
const pair = keyring.addFromAddress(address, objectSpread({}, meta1, { isExternal: true }), null); | ||
console.log("MultiSig address created::", pair.address); | ||
return pair.address; | ||
} |
96 changes: 96 additions & 0 deletions
96
examples/substrate/use-futurepass-multisig/src/proxyExtrinsic.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { u8aToHex } from "@polkadot/util"; | ||
import { createDispatcher, nativeWalletSigner } from "@therootnetwork/extrinsic"; | ||
import { createKeyring } from "@trne/utils/createKeyring"; | ||
import { withChainContext } from "@trne/utils/withChainContext"; | ||
import { cleanEnv, str } from "envalid"; | ||
import assert from "node:assert"; | ||
|
||
const { CHAIN_ENDPOINT, SIGNATORIES, CALLER_PRIVATE_KEY } = cleanEnv(process.env, { | ||
CHAIN_ENDPOINT: str({ default: "porcini" }), | ||
SIGNATORIES: str(), // Comma separated signatories | ||
CALLER_PRIVATE_KEY: str(), // private key of extrinsic caller to fund multi wallet address | ||
}); | ||
|
||
/** | ||
* Use `futurepass.proxyExtrinsic` to call `system.remarkWithEvent` and have FPasss account | ||
* to pay for gas. | ||
* | ||
* Assume FPass account of the caller has XRP to pay for gas. | ||
*/ | ||
withChainContext(CHAIN_ENDPOINT, async (api, logger) => { | ||
const caller = createKeyring(CALLER_PRIVATE_KEY); | ||
const fpAccount = (await api.query.futurepass.holders(caller.address)).unwrapOr(undefined); | ||
assert(fpAccount); | ||
logger.info( | ||
{ | ||
futurepass: { | ||
holder: caller.address, | ||
account: fpAccount.toString(), | ||
}, | ||
}, | ||
"futurepass details" | ||
); | ||
|
||
const multiSigCall = api.tx.system.remarkWithEvent("Hello World"); | ||
const u8a = multiSigCall.method.toU8a(); | ||
const encodedCallData = u8aToHex(u8a); | ||
|
||
const signatoryList = SIGNATORIES.split(","); | ||
console.log("signatoryList::", signatoryList); | ||
const threshold = signatoryList.length; | ||
const maybeTimepoint = null; | ||
const storeCall = false; | ||
const maxWeight = 0; | ||
|
||
const call = await api.tx.multisig.asMulti( | ||
threshold, | ||
signatoryList, | ||
maybeTimepoint, | ||
encodedCallData, | ||
storeCall, | ||
maxWeight | ||
); | ||
|
||
const proxyExtrinsic = await api.tx.futurepass.proxyExtrinsic(fpAccount, call); | ||
logger.info( | ||
{ | ||
parameters: { | ||
fpAccount, | ||
call, | ||
}, | ||
}, | ||
`create a "futurepass.proxyExtrinsic" extrinsic` | ||
); | ||
const { estimate, signAndSend } = createDispatcher( | ||
api, | ||
caller.address, | ||
[], | ||
nativeWalletSigner(caller) | ||
); | ||
|
||
const feeResult = await estimate(proxyExtrinsic); | ||
assert(feeResult.ok, (feeResult.value as Error).message); | ||
logger.info( | ||
{ parameters: { caller: caller.address, fee: feeResult.ok ? feeResult.value : undefined } }, | ||
`dispatch extrinsic` | ||
); | ||
|
||
const result = await signAndSend(proxyExtrinsic, (status) => { | ||
logger.debug(status); | ||
}); | ||
assert(result.ok, (result.value as Error).message); | ||
|
||
const { id, events } = result.value; | ||
console.log("events:", events); | ||
const multiSigEvent = events.find((event) => event.name === "multisig.NewMultisig"); | ||
assert(multiSigEvent); | ||
logger.info( | ||
{ | ||
result: { | ||
extrinsicId: id, | ||
multiSigEvent, | ||
}, | ||
}, | ||
"dispatch result" | ||
); | ||
}); |
113 changes: 113 additions & 0 deletions
113
examples/substrate/use-futurepass-multisig/src/signByOtherWallet.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { u8aToHex } from "@polkadot/util"; | ||
import { createDispatcher, nativeWalletSigner } from "@therootnetwork/extrinsic"; | ||
import { createKeyring } from "@trne/utils/createKeyring"; | ||
import { withChainContext } from "@trne/utils/withChainContext"; | ||
import { cleanEnv, str } from "envalid"; | ||
import assert from "node:assert"; | ||
|
||
const { CHAIN_ENDPOINT, SIGNATORIES, MULTISIG_WALLET, THRESHOLD, CALLER_PRIVATE_KEY } = cleanEnv( | ||
process.env, | ||
{ | ||
CHAIN_ENDPOINT: str({ default: "porcini" }), | ||
SIGNATORIES: str(), // Comma separated signatories | ||
MULTISIG_WALLET: str(), // private key of extrinsic caller | ||
THRESHOLD: str(), | ||
CALLER_PRIVATE_KEY: str(), // private key of extrinsic caller to fund multi wallet address | ||
} | ||
); | ||
|
||
/** | ||
* Sign multisig extrinisc with other signer | ||
* | ||
*/ | ||
withChainContext(CHAIN_ENDPOINT, async (api, logger) => { | ||
const caller = createKeyring(CALLER_PRIVATE_KEY); | ||
|
||
logger.info( | ||
{ | ||
multisigWallet: { | ||
holder: caller.address, | ||
}, | ||
}, | ||
"futurepass details" | ||
); | ||
|
||
const multiSigCall = api.tx.system.remarkWithEvent("Hello World"); | ||
const u8a = multiSigCall.method.toU8a(); | ||
const encodedCallData = u8aToHex(u8a); | ||
|
||
const signatoryList = SIGNATORIES.split(","); | ||
console.log("signatoryList::", signatoryList); | ||
const threshold = signatoryList.length; | ||
const storeCall = false; | ||
|
||
let timepoint = {}; | ||
const allEntries = await api.query.multisig.multisigs.entries(MULTISIG_WALLET); | ||
allEntries.forEach( | ||
([ | ||
{ | ||
args: [accountId], | ||
}, | ||
value, | ||
]) => { | ||
const time = JSON.parse(value); | ||
timepoint = time.when; | ||
} | ||
); | ||
const maybeTimepoint = api.registry.createType("Option<Timepoint>", timepoint); | ||
const maxWeight = 882400098; | ||
console.log("maybeTimepointData::", maybeTimepoint.toHuman()); | ||
|
||
const call = await api.tx.multisig.asMulti( | ||
threshold, | ||
signatoryList, | ||
maybeTimepoint, | ||
encodedCallData, | ||
storeCall, | ||
maxWeight | ||
); | ||
|
||
logger.info( | ||
{ | ||
parameters: { | ||
caller: caller.address, | ||
call, | ||
}, | ||
}, | ||
`create a "futurepass.proxyExtrinsic" extrinsic` | ||
); | ||
const { estimate, signAndSend } = createDispatcher( | ||
api, | ||
caller.address, | ||
[], | ||
nativeWalletSigner(caller) | ||
); | ||
|
||
const feeResult = await estimate(call); | ||
assert(feeResult.ok, (feeResult.value as Error).message); | ||
logger.info( | ||
{ parameters: { caller: caller.address, fee: feeResult.ok ? feeResult.value : undefined } }, | ||
`dispatch extrinsic` | ||
); | ||
|
||
const result = await signAndSend(call, (status) => { | ||
logger.debug(status); | ||
}); | ||
assert(result.ok, (result.value as Error).message); | ||
|
||
const { id, events } = result.value; | ||
console.log("events::", events); | ||
const multiSigEvent = events.find((event) => event.name === "multisig.MultisigExecuted"); | ||
assert(multiSigEvent); | ||
const systemRemarkEvent = events.find((event) => event.name === "system.Remarked"); | ||
assert(systemRemarkEvent); | ||
logger.info( | ||
{ | ||
result: { | ||
extrinsicId: id, | ||
multiSigEvent, | ||
}, | ||
}, | ||
"dispatch result" | ||
); | ||
}); |