Skip to content

Commit

Permalink
feat: multisig support for contract deploy transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
fess-v committed Aug 4, 2023
1 parent df36f25 commit af81c02
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 22 deletions.
87 changes: 65 additions & 22 deletions packages/transactions/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import { ClarityAbi, validateContractCall } from './contract-abi';
import { NoEstimateAvailableError } from './errors';
import {
createStacksPrivateKey,
createStacksPublicKey,
getPublicKey,
pubKeyfromPrivKey,
publicKeyFromBytes,
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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<StacksTransaction> {
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<StacksTransaction> {
const defaultOptions = {
fee: BigInt(0),
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
30 changes: 30 additions & 0 deletions packages/transactions/tests/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit af81c02

Please sign in to comment.