Skip to content

Commit

Permalink
feat: enable creation of unsigned contract calls
Browse files Browse the repository at this point in the history
  • Loading branch information
yknl committed Oct 22, 2020
1 parent 30d1fff commit ba2243e
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 24 deletions.
4 changes: 2 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
estimateContractFunctionCall,
SignedTokenTransferOptions,
ContractDeployOptions,
ContractCallOptions,
SignedContractCallOptions,
ReadOnlyFunctionOptions,
ContractCallPayload,
ClarityValue,
Expand Down Expand Up @@ -670,7 +670,7 @@ async function contractFunctionCall(network: CLINetworkAdapter, args: string[]):
.then(answers => {
functionArgs = parseClarityFunctionArgAnswers(answers, abiArgs);

const options: ContractCallOptions = {
const options: SignedContractCallOptions = {
contractAddress,
contractName,
functionName,
Expand Down
108 changes: 86 additions & 22 deletions packages/transactions/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ export interface SignedMultiSigTokenTransferOptions extends TokenTransferOptions
*
* @param {UnsignedTokenTransferOptions | UnsignedMultiSigTokenTransferOptions} txOptions - an options object for the token transfer
*
* @return {StacksTransaction}
* @return {Promis<StacksTransaction>}
*/
export async function makeUnsignedSTXTokenTransfer(
txOptions: UnsignedTokenTransferOptions | UnsignedMultiSigTokenTransferOptions
Expand Down Expand Up @@ -614,7 +614,6 @@ export interface ContractCallOptions {
contractName: string;
functionName: string;
functionArgs: ClarityValue[];
senderKey: string;
fee?: BigNum;
feeEstimateApiUrl?: string;
nonce?: BigNum;
Expand All @@ -626,6 +625,25 @@ export interface ContractCallOptions {
sponsored?: boolean;
}

export interface UnsignedContractCallOptions extends ContractCallOptions {
publicKey: string;
}

export interface SignedContractCallOptions extends ContractCallOptions {
senderKey: string;
}

export interface UnsignedMultiSigContractCallOptions extends ContractCallOptions {
numSignatures: number;
publicKeys: string[];
}

export interface SignedMultiSigContractCallOptions extends ContractCallOptions {
numSignatures: number;
publicKeys: string[];
signerKeys: string[];
}

/**
* Estimate the total transaction fee in microstacks for a contract function call
*
Expand Down Expand Up @@ -679,15 +697,15 @@ export async function estimateContractFunctionCall(
}

/**
* Generates a Clarity smart contract function call transaction
* Generates an unsigned Clarity smart contract function call transaction
*
* @param {ContractCallOptions} txOptions - an options object for the contract function call
* @param {UnsignedContractCallOptions | UnsignedMultiSigContractCallOptions} txOptions - an options object for the contract call
*
* Returns a signed Stacks smart contract function call transaction.
*
* @return {StacksTransaction}
* @returns {Promise<StacksTransaction>}
*/
export async function makeContractCall(txOptions: ContractCallOptions): Promise<StacksTransaction> {
export async function makeUnsignedContractCall(
txOptions: UnsignedContractCallOptions | UnsignedMultiSigContractCallOptions
): Promise<StacksTransaction> {
const defaultOptions = {
fee: new BigNum(0),
nonce: new BigNum(0),
Expand Down Expand Up @@ -721,18 +739,27 @@ export async function makeContractCall(txOptions: ContractCallOptions): Promise<
validateContractCall(payload, abi);
}

const addressHashMode = AddressHashMode.SerializeP2PKH;
const privKey = createStacksPrivateKey(options.senderKey);
const pubKey = getPublicKey(privKey);

let spendingCondition = null;
let authorization = null;

const spendingCondition = createSingleSigSpendingCondition(
addressHashMode,
publicKeyToString(pubKey),
options.nonce,
options.fee
);
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 = new SponsoredAuthorization(spendingCondition);
Expand Down Expand Up @@ -768,17 +795,54 @@ export async function makeContractCall(txOptions: ContractCallOptions): Promise<
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);
}

if (options.senderKey) {
return transaction;
}

/**
* Generates a Clarity smart contract function call transaction
*
* @param {SignedContractCallOptions | SignedMultiSigContractCallOptions} txOptions - an options object for the contract function call
*
* Returns a signed Stacks smart contract function call transaction.
*
* @return {StacksTransaction}
*/
export async function makeContractCall(
txOptions: SignedContractCallOptions | SignedMultiSigContractCallOptions
): Promise<StacksTransaction> {
if ('senderKey' in txOptions) {
const publicKey = publicKeyToString(getPublicKey(createStacksPrivateKey(txOptions.senderKey)));
const options = omit(txOptions, 'senderKey');
const transaction = await makeUnsignedContractCall({ publicKey, ...options });

const privKey = createStacksPrivateKey(txOptions.senderKey);
const signer = new TransactionSigner(transaction);
signer.signOrigin(privKey);
}

return transaction;
return transaction;
} else {
const options = omit(txOptions, 'signerKeys');
const transaction = await makeUnsignedContractCall(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 !== pubKey.data.toString('hex'));
signer.signOrigin(createStacksPrivateKey(key));
}

for (const key of pubKeys) {
signer.appendOrigin(publicKeyFromBuffer(Buffer.from(key, 'hex')));
}

return transaction;
}
}

/**
Expand Down
71 changes: 71 additions & 0 deletions packages/transactions/tests/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
callReadOnlyFunction,
sponsorTransaction,
makeSTXTokenTransfer,
makeUnsignedContractCall
} from '../src/builders';

import { deserializeTransaction } from '../src/transaction';
Expand Down Expand Up @@ -479,6 +480,76 @@ test('Make contract-call with post condition allow mode', async () => {
expect(serialized).toBe(tx);
});

test('addSignature to an unsigned contract call transaction', async () => {
const contractAddress = 'ST3KC0MTNW34S1ZXD36JYKFD3JJMWA01M55DSJ4JE';
const contractName = 'kv-store';
const functionName = 'get-value';
const buffer = bufferCV(Buffer.from('foo'));
const fee = new BigNum(0);
const publicKey = '021ae7f08f9eaecaaa93f7c6ceac29213bae09588c15e2aded32016b259cfd9a1f';

const unsignedTx = await makeUnsignedContractCall({
contractAddress,
contractName,
functionName,
functionArgs: [buffer],
publicKey,
fee,
nonce: new BigNum(1),
network: new StacksTestnet(),
postConditionMode: PostConditionMode.Allow,
});

const nullSignature = (unsignedTx.auth.spendingCondition as any).signature.data;

expect(nullSignature).toEqual(
'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
);

const sig =
'00e4ee626905ee9d04b786e2942a69504dcc0f35ca79b86fb0aafcd47a81fc3bf1547e302c3acf5c89d935a53df334316e6fcdc203cf6bed91288ebf974385398c';
const signedTx = unsignedTx.createTxWithSignature(sig);
expect((signedTx.auth.spendingCondition as SingleSigSpendingCondition).signature.data).toEqual(
sig
);
expect(unsignedTx).not.toBe(signedTx);
});

test('make a multi-sig contract call', async () => {
const contractAddress = 'ST3KC0MTNW34S1ZXD36JYKFD3JJMWA01M55DSJ4JE';
const contractName = 'kv-store';
const functionName = 'get-value';
const buffer = bufferCV(Buffer.from('foo'));
const fee = new BigNum(0);
const privKeyStrings = [
'6d430bb91222408e7706c9001cfaeb91b08c2be6d5ac95779ab52c6b431950e001',
'2a584d899fed1d24e26b524f202763c8ab30260167429f157f1c119f550fa6af01',
'd5200dee706ee53ae98a03fba6cf4fdcc5084c30cfa9e1b3462dcdeaa3e0f1d201',
];
// const privKeys = privKeyStrings.map(createStacksPrivateKey);

const pubKeys = privKeyStrings.map(pubKeyfromPrivKey);
const pubKeyStrings = pubKeys.map(publicKeyToString);

const tx = await makeContractCall({
contractAddress,
contractName,
functionName,
functionArgs: [buffer],
publicKeys: pubKeyStrings,
numSignatures: 3,
signerKeys: privKeyStrings,
fee,
nonce: new BigNum(1),
network: new StacksTestnet(),
postConditionMode: PostConditionMode.Allow,
});

expect(tx.auth.spendingCondition!.signer).toEqual(
'04128cacf0764f69b1e291f62d1dcdd8f65be5ab'
);
});

test('Estimate token transfer fee', async () => {
const apiUrl = `${DEFAULT_CORE_NODE_API_URL}/v2/fees/transfer`;
const estimateFeeRate = 1;
Expand Down

0 comments on commit ba2243e

Please sign in to comment.