Skip to Content
Supra Layer 1SupraMVMTypeScript SDKGuidesMultisig Accounts

Multisig Accounts

How to create a multisig account, propose a transaction, approve or reject it, and execute it with the TypeScript SDK.

supra.multisig covers the lifecycle of a multisig account (0x1::multisig_account). The methods that send a transaction take the sender’s SupraAccount and the usual optionalTransactionArgs. Like the other helpers, they use the client’s sequence numbers, the chain ID check, the estimated max gas and the node’s gas unit price estimate.

TaskSDK function
Create a multisig accountsupra.multisig.createMultisigAccount
Propose, payload stored on chainsupra.multisig.proposeMultisigTransaction
Propose, only the payload hashsupra.multisig.proposeMultisigTransactionWithHash
Approve a proposalsupra.multisig.approveMultisigTransaction
Reject a proposalsupra.multisig.rejectMultisigTransaction
Execute the next proposalsupra.multisig.executeMultisigTransaction
Remove a rejected or expired onesupra.multisig.executeRejectedMultisigTransaction
Read the queue and the proposalsgetMultisigSequenceNumbers, getMultisigProposal, getNextMultisigProposal

How a multisig account works

A multisig account has a list of owners and a number of required signatures. An owner proposes an entry function, the owners vote on the proposal, and when enough owners have approved it, an owner executes it. The multisig account is the signer of the executed function. The sender of the execution pays the gas.

Proposals form a queue and are resolved in order. A proposal’s number starts at 1. getMultisigSequenceNumbers returns { lastResolved, next }: the number of the last proposal that was executed or removed (0 before the first) and the number the next new proposal gets. Proposals lastResolved + 1 to next - 1 are pending, and executeMultisigTransaction and executeRejectedMultisigTransaction act on proposal lastResolved + 1.

Set up the project

mkdir supra-multisig && cd supra-multisig npm init -y npm pkg set type=module npm install supra-ts-sdk@devnet npm install --save-dev tsx

Create multisig_example.ts

The example uses Devnet. It generates two owners, funds them from the faucet, creates a multisig account that needs both, and executes a transfer from the multisig account. Use Network.TESTNET for Testnet.

import { Network, SupraAccount, SupraClient } from 'supra-ts-sdk' const supra = new SupraClient({ network: Network.DEVNET }) const waitForCommit = { enableTransactionWaitAndSimulationArgs: { enableWaitForTransaction: true } } async function fund(account: SupraAccount) { const faucet = await supra.faucet.fundAccountWithFaucet({ accountAddress: account.address() }) await supra.transaction.waitForTransaction({ transactionHash: faucet.hash }) } async function main() { const first = new SupraAccount() const second = new SupraAccount() await fund(first) await fund(second) // Create a multisig account with two owners that needs both signatures. const { multisigAddress } = await supra.multisig.createMultisigAccount({ senderAccount: first, additionalOwners: [second.address()], signaturesRequired: 2, optionalTransactionArgs: waitForCommit, }) console.log('Multisig account:', multisigAddress) // The transfer below moves SUPRA out of the multisig account, so it needs a balance. await supra.coin.transferSupraCoin({ senderAccount: first, receiverAccountAddress: multisigAddress, amount: 10_000n, optionalTransactionArgs: waitForCommit, }) // Propose a transfer. The payload is stored on chain. const receiver = new SupraAccount().address() await supra.multisig.proposeMultisigTransaction({ senderAccount: first, multisigAddress, payload: { function: '0x1::supra_account::transfer', functionArgs: [receiver.toString(), 1_000n] }, optionalTransactionArgs: waitForCommit, }) // The proposal that was just created is the last pending one. const { next } = await supra.multisig.getMultisigSequenceNumbers({ multisigAddress }) const sequenceNumber = next - 1n const proposal = await supra.multisig.getMultisigProposal({ multisigAddress, sequenceNumber }) console.log('Votes:', proposal.votes) // The second owner approves. The proposer's approval is already counted. await supra.multisig.approveMultisigTransaction({ senderAccount: second, multisigAddress, sequenceNumber, optionalTransactionArgs: waitForCommit, }) // Any owner can execute the next proposal once it has enough approvals. const executed = await supra.multisig.executeMultisigTransaction({ senderAccount: first, multisigAddress, optionalTransactionArgs: waitForCommit, }) console.log('Execution:', executed.status, executed.hash) } main()

Run it

npx tsx multisig_example.ts

createMultisigAccount returns { multisigAddress, transaction }. The address is derived from the sender and the sequence number of the creating transaction (deriveMultisigAccountAddress), and the account exists once that transaction has executed. signaturesRequired is the number of approvals a proposal needs. timeoutDurationSecs sets how long a proposal stays valid; the default is seven days (DEFAULT_MULTISIG_TIMEOUT_DURATION_SECS).

The propose and approve calls in the example wait for their commit so that the sequence number read and the execution see them.

Propose with a stored payload or with a hash

proposeMultisigTransaction (create_transaction) stores the whole payload on chain. The executor needs no payload: executeMultisigTransaction reads the next proposal and sends the execution without one.

proposeMultisigTransactionWithHash (create_transaction_with_hash) stores only the SHA3-256 hash of the payload. The executor passes the payload to executeMultisigTransaction, which checks it against the stored hash before it submits. It throws SupraMultisigPayloadError when the payload is missing or does not match the hash.

const payload = { function: '0x1::supra_account::transfer', functionArgs: [receiver.toString(), 1_000n] } await supra.multisig.proposeMultisigTransactionWithHash({ senderAccount: first, multisigAddress, payload, optionalTransactionArgs: waitForCommit, }) // ... the owners approve ... await supra.multisig.executeMultisigTransaction({ senderAccount: first, multisigAddress, payload, optionalTransactionArgs: waitForCommit, })

A payload has a function, optional functionTypeArgs and functionArgs. The arguments are encoded with the ABI of the function, as for supra.transaction.build.simple, and do not include the leading &signer parameter, which is the multisig account.

executeMultisigTransaction throws SupraMultisigNoProposalError when the queue is empty.

Reject a proposal

An owner who rejects a proposal casts a rejection; an owner who had approved changes the vote. With enough rejections, or after the proposal has timed out, any owner can remove it from the queue with executeRejectedMultisigTransaction. The sender’s own rejection counts. A proposal that cannot be executed blocks the queue until it is removed this way.

await supra.multisig.rejectMultisigTransaction({ senderAccount: second, multisigAddress, sequenceNumber, optionalTransactionArgs: waitForCommit, }) await supra.multisig.executeRejectedMultisigTransaction({ senderAccount: first, multisigAddress, optionalTransactionArgs: waitForCommit, })

Read a multisig account

FunctionReturns
getMultisigSequenceNumbers({ multisigAddress }){ lastResolved, next }, both bigint
getMultisigProposal({ multisigAddress, sequenceNumber })A MultisigProposal: sequenceNumber, payload, payloadHash, votes, creator, creationTimeSecs
getNextMultisigProposal({ multisigAddress })The next proposal to execute or remove, or undefined when the queue is empty

A proposal has either a payload (the BCS bytes of the stored payload as hex) or a payloadHash; the other is undefined. Each entry of votes has an owner and approved, which is true for an approval and false for a rejection. The reads throw SupraAPIError when the address is not a multisig account.

Simulation

The nodes do not simulate an execution that carries no payload. executeMultisigTransaction therefore simulates the execution with the stored payload for its max gas estimate and for enableTransactionSimulation, and submits the execution without it.

A simulation does not check the approvals, the timeout or the payload against the proposal. enableTransactionSimulation does not detect missing approvals or an expired proposal; the node refuses such an execution when it validates it.

For the SDK’s other transaction helpers, see the TypeScript SDK overview.

Last updated on