From af81c0221b7f401758342d55067147702fb91e01 Mon Sep 17 00:00:00 2001 From: Fess Date: Fri, 4 Aug 2023 19:34:51 +0400 Subject: [PATCH 1/2] feat: multisig support for contract deploy transactions --- packages/transactions/src/builders.ts | 87 +++++++++++++++------ packages/transactions/tests/builder.test.ts | 30 +++++++ 2 files changed, 95 insertions(+), 22 deletions(-) diff --git a/packages/transactions/src/builders.ts b/packages/transactions/src/builders.ts index 9fb6b2070..4caa3a081 100644 --- a/packages/transactions/src/builders.ts +++ b/packages/transactions/src/builders.ts @@ -38,7 +38,6 @@ import { ClarityAbi, validateContractCall } from './contract-abi'; import { NoEstimateAvailableError } from './errors'; import { createStacksPrivateKey, - createStacksPublicKey, getPublicKey, pubKeyfromPrivKey, publicKeyFromBytes, @@ -717,6 +716,21 @@ export interface UnsignedContractDeployOptions extends BaseContractDeployOptions publicKey: string; } +export interface SignedContractDeployOptions extends BaseContractDeployOptions { + senderKey: string; +} + +export interface UnsignedMultiSigContractDeployOptions extends BaseContractDeployOptions { + numSignatures: number; + publicKeys: string[]; +} + +export interface SignedMultiSigContractDeployOptions extends BaseContractDeployOptions { + numSignatures: number; + publicKeys: string[]; + signerKeys: string[]; +} + /** * @deprecated Use the new {@link estimateTransaction} function insterad. * @@ -772,31 +786,49 @@ export async function estimateContractDeploy( /** * Generates a Clarity smart contract deploy transaction * - * @param {ContractDeployOptions} txOptions - an options object for the contract deploy + * @param {SignedContractDeployOptions | SignedMultiSigContractDeployOptions} txOptions - an options object for the contract deploy * * Returns a signed Stacks smart contract deploy transaction. * * @return {StacksTransaction} */ export async function makeContractDeploy( - txOptions: ContractDeployOptions + txOptions: SignedContractDeployOptions | SignedMultiSigContractDeployOptions ): Promise { - const privKey = createStacksPrivateKey(txOptions.senderKey); - const stacksPublicKey = getPublicKey(privKey); - const publicKey = publicKeyToString(stacksPublicKey); - const unsignedTxOptions: UnsignedContractDeployOptions = { ...txOptions, publicKey }; - const transaction: StacksTransaction = await makeUnsignedContractDeploy(unsignedTxOptions); + if ('senderKey' in txOptions) { + // txOptions is SignedContractDeployOptions + const publicKey = publicKeyToString(getPublicKey(createStacksPrivateKey(txOptions.senderKey))); + const options = omit(txOptions, 'senderKey'); + const transaction = await makeUnsignedContractDeploy({ publicKey, ...options }); - if (txOptions.senderKey) { + const privKey = createStacksPrivateKey(txOptions.senderKey); const signer = new TransactionSigner(transaction); signer.signOrigin(privKey); - } - return transaction; + return transaction; + } else { + // txOptions is SignedMultiSigContractDeployOptions + const options = omit(txOptions, 'signerKeys'); + const transaction = await makeUnsignedContractDeploy(options); + + const signer = new TransactionSigner(transaction); + let pubKeys = txOptions.publicKeys; + for (const key of txOptions.signerKeys) { + const pubKey = pubKeyfromPrivKey(key); + pubKeys = pubKeys.filter(pk => pk !== bytesToHex(pubKey.data)); + signer.signOrigin(createStacksPrivateKey(key)); + } + + for (const key of pubKeys) { + signer.appendOrigin(publicKeyFromBytes(hexToBytes(key))); + } + + return transaction; + } } export async function makeUnsignedContractDeploy( - txOptions: UnsignedContractDeployOptions + txOptions: UnsignedContractDeployOptions | UnsignedMultiSigContractDeployOptions ): Promise { const defaultOptions = { fee: BigInt(0), @@ -815,17 +847,28 @@ export async function makeUnsignedContractDeploy( options.clarityVersion ); - const addressHashMode = AddressHashMode.SerializeP2PKH; - const pubKey = createStacksPublicKey(options.publicKey); - let authorization: Authorization | null = null; - const spendingCondition = createSingleSigSpendingCondition( - addressHashMode, - publicKeyToString(pubKey), - options.nonce, - options.fee - ); + let spendingCondition: SpendingCondition | null = null; + + if ('publicKey' in options) { + // single-sig + spendingCondition = createSingleSigSpendingCondition( + AddressHashMode.SerializeP2PKH, + options.publicKey, + options.nonce, + options.fee + ); + } else { + // multi-sig + spendingCondition = createMultiSigSpendingCondition( + AddressHashMode.SerializeP2SH, + options.numSignatures, + options.publicKeys, + options.nonce, + options.fee + ); + } if (options.sponsored) { authorization = createSponsoredAuth(spendingCondition); @@ -863,7 +906,7 @@ export async function makeUnsignedContractDeploy( options.network.version === TransactionVersion.Mainnet ? AddressVersion.MainnetSingleSig : AddressVersion.TestnetSingleSig; - const senderAddress = publicKeyToAddress(addressVersion, pubKey); + const senderAddress = c32address(addressVersion, transaction.auth.spendingCondition!.signer); const txNonce = await getNonce(senderAddress, options.network); transaction.setNonce(txNonce); } diff --git a/packages/transactions/tests/builder.test.ts b/packages/transactions/tests/builder.test.ts index 9e6371c27..b469a362a 100644 --- a/packages/transactions/tests/builder.test.ts +++ b/packages/transactions/tests/builder.test.ts @@ -807,6 +807,36 @@ test('Make smart contract deploy unsigned', async () => { expect(deserializedTx.auth.spendingCondition!.fee!.toString()).toBe(fee.toString()); }); +test('make a multi-sig contract deploy', async () => { + const contractName = 'kv-store'; + const codeBody = fs.readFileSync('./tests/contracts/kv-store.clar').toString(); + const fee = 0; + const nonce = 0; + const privKeyStrings = [ + '6d430bb91222408e7706c9001cfaeb91b08c2be6d5ac95779ab52c6b431950e001', + '2a584d899fed1d24e26b524f202763c8ab30260167429f157f1c119f550fa6af01', + 'd5200dee706ee53ae98a03fba6cf4fdcc5084c30cfa9e1b3462dcdeaa3e0f1d201', + ]; + // const privKeys = privKeyStrings.map(createStacksPrivateKey); + + const pubKeys = privKeyStrings.map(pubKeyfromPrivKey); + const pubKeyStrings = pubKeys.map(publicKeyToString); + + const tx = await makeContractDeploy({ + codeBody, + contractName, + publicKeys: pubKeyStrings, + numSignatures: 3, + signerKeys: privKeyStrings, + fee, + nonce, + network: new StacksTestnet(), + anchorMode: AnchorMode.Any, + }); + + expect(tx.auth.spendingCondition!.signer).toEqual('04128cacf0764f69b1e291f62d1dcdd8f65be5ab'); +}); + test('Make smart contract deploy signed', async () => { const contractName = 'kv-store'; const codeBody = fs.readFileSync('./tests/contracts/kv-store.clar').toString(); From 5f1c6479b2b126b2591c44a818283cfdffd13f81 Mon Sep 17 00:00:00 2001 From: Fess Date: Fri, 4 Aug 2023 19:47:13 +0400 Subject: [PATCH 2/2] fix: contract deploy options type removed --- packages/cli/src/cli.ts | 4 ++-- packages/transactions/src/builders.ts | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 73887da10..b6cdb9db9 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -18,7 +18,7 @@ import { ClarityAbi, ClarityValue, ContractCallPayload, - ContractDeployOptions, + SignedContractDeployOptions, createStacksPrivateKey, cvToString, estimateContractDeploy, @@ -766,7 +766,7 @@ async function contractDeploy(network: CLINetworkAdapter, args: string[]): Promi ? new StacksMainnet({ url: network.legacyNetwork.blockstackAPIUrl }) : new StacksTestnet({ url: network.legacyNetwork.blockstackAPIUrl }); - const options: ContractDeployOptions = { + const options: SignedContractDeployOptions = { contractName, codeBody: source, senderKey: privateKey, diff --git a/packages/transactions/src/builders.ts b/packages/transactions/src/builders.ts index 4caa3a081..16467a4a7 100644 --- a/packages/transactions/src/builders.ts +++ b/packages/transactions/src/builders.ts @@ -706,11 +706,6 @@ export interface BaseContractDeployOptions { sponsored?: boolean; } -export interface ContractDeployOptions extends BaseContractDeployOptions { - /** a hex string of the private key of the transaction sender */ - senderKey: string; -} - export interface UnsignedContractDeployOptions extends BaseContractDeployOptions { /** a hex string of the public key of the transaction sender */ publicKey: string;