diff --git a/packages/transactions/src/builders.ts b/packages/transactions/src/builders.ts index b0a7981a23..b6d66ee9f3 100644 --- a/packages/transactions/src/builders.ts +++ b/packages/transactions/src/builders.ts @@ -31,6 +31,7 @@ import { TxRejectedReason, RECOVERABLE_ECDSA_SIG_LENGTH_BYTES, StacksMessageType, + ClarityVersion, } from './constants'; import { ClarityAbi, validateContractCall } from './contract-abi'; import { @@ -689,6 +690,7 @@ export async function makeSTXTokenTransfer( * Contract deploy transaction options */ export interface BaseContractDeployOptions { + clarityVersion?: ClarityVersion; contractName: string; /** the Clarity code to be deployed */ codeBody: string; @@ -734,7 +736,10 @@ export async function estimateContractDeploy( transaction: StacksTransaction, network?: StacksNetworkName | StacksNetwork ): Promise { - if (transaction.payload.payloadType !== PayloadType.SmartContract) { + if ( + transaction.payload.payloadType !== PayloadType.SmartContract && + transaction.payload.payloadType !== PayloadType.VersionedSmartContract + ) { throw new Error( `Contract deploy fee estimation only possible with ${ PayloadType[PayloadType.SmartContract] @@ -808,7 +813,11 @@ export async function makeUnsignedContractDeploy( const options = Object.assign(defaultOptions, txOptions); - const payload = createSmartContractPayload(options.contractName, options.codeBody); + const payload = createSmartContractPayload( + options.contractName, + options.codeBody, + options.clarityVersion + ); const addressHashMode = AddressHashMode.SerializeP2PKH; const pubKey = createStacksPublicKey(options.publicKey); @@ -1384,6 +1393,7 @@ export async function sponsorTransaction( switch (options.transaction.payload.payloadType) { case PayloadType.TokenTransfer: case PayloadType.SmartContract: + case PayloadType.VersionedSmartContract: case PayloadType.ContractCall: const estimatedLen = estimateTransactionByteLength(options.transaction); try { diff --git a/packages/transactions/src/constants.ts b/packages/transactions/src/constants.ts index 2462f58678..9c65f8b39d 100644 --- a/packages/transactions/src/constants.ts +++ b/packages/transactions/src/constants.ts @@ -41,11 +41,17 @@ export function whenMessageType(messageType: StacksMessageType) { enum PayloadType { TokenTransfer = 0x00, SmartContract = 0x01, + VersionedSmartContract = 0x06, ContractCall = 0x02, PoisonMicroblock = 0x03, Coinbase = 0x04, } +enum ClarityVersion { + Clarity1 = 1, + Clarity2 = 2, +} + /** * How a transaction should get appended to the Stacks blockchain. * @@ -178,6 +184,7 @@ export { ChainID, StacksMessageType, PayloadType, + ClarityVersion, AnchorMode, TransactionVersion, PostConditionMode, diff --git a/packages/transactions/src/index.ts b/packages/transactions/src/index.ts index 34e6dc83e8..1e812e9ec6 100644 --- a/packages/transactions/src/index.ts +++ b/packages/transactions/src/index.ts @@ -15,6 +15,7 @@ export { TokenTransferPayload, ContractCallPayload, SmartContractPayload, + VersionedSmartContractPayload, PoisonPayload, CoinbasePayload, serializePayload, diff --git a/packages/transactions/src/payload.ts b/packages/transactions/src/payload.ts index 79eb216f0f..437a7993df 100644 --- a/packages/transactions/src/payload.ts +++ b/packages/transactions/src/payload.ts @@ -1,26 +1,26 @@ import { concatArray, IntegerType, intToBigInt, intToBytes, writeUInt32BE } from '@stacks/common'; -import { COINBASE_BYTES_LENGTH, PayloadType, StacksMessageType } from './constants'; +import { ClarityVersion, COINBASE_BYTES_LENGTH, PayloadType, StacksMessageType } from './constants'; +import { BytesReader } from './bytesReader'; +import { ClarityValue, deserializeCV, serializeCV } from './clarity/'; +import { PrincipalCV, principalCV } from './clarity/types/principalCV'; +import { Address } from './common'; +import { createAddress, createLPString, LengthPrefixedString } from './postcondition-types'; import { - MemoString, + codeBodyString, createMemoString, - serializeStacksMessage, deserializeAddress, deserializeLPString, deserializeMemoString, - codeBodyString, + MemoString, + serializeStacksMessage, } from './types'; -import { createAddress, LengthPrefixedString, createLPString } from './postcondition-types'; -import { Address } from './common'; -import { ClarityValue, serializeCV, deserializeCV } from './clarity/'; - -import { BytesReader } from './bytesReader'; -import { PrincipalCV, principalCV } from './clarity/types/principalCV'; export type Payload = | TokenTransferPayload | ContractCallPayload | SmartContractPayload + | VersionedSmartContractPayload | PoisonPayload | CoinbasePayload; @@ -36,6 +36,7 @@ export type PayloadInput = | (TokenTransferPayload | (Omit & { amount: IntegerType })) | ContractCallPayload | SmartContractPayload + | VersionedSmartContractPayload | PoisonPayload | CoinbasePayload; @@ -102,10 +103,19 @@ export interface SmartContractPayload { readonly codeBody: LengthPrefixedString; } +export interface VersionedSmartContractPayload { + readonly type: StacksMessageType.Payload; + readonly payloadType: PayloadType.VersionedSmartContract; + readonly clarityVersion: ClarityVersion; + readonly contractName: LengthPrefixedString; + readonly codeBody: LengthPrefixedString; +} + export function createSmartContractPayload( contractName: string | LengthPrefixedString, - codeBody: string | LengthPrefixedString -): SmartContractPayload { + codeBody: string | LengthPrefixedString, + clarityVersion?: ClarityVersion +): SmartContractPayload | VersionedSmartContractPayload { if (typeof contractName === 'string') { contractName = createLPString(contractName); } @@ -113,12 +123,22 @@ export function createSmartContractPayload( codeBody = codeBodyString(codeBody); } - return { - type: StacksMessageType.Payload, - payloadType: PayloadType.SmartContract, - contractName, - codeBody, - }; + if (typeof clarityVersion === 'number') { + return { + type: StacksMessageType.Payload, + payloadType: PayloadType.VersionedSmartContract, + clarityVersion: clarityVersion, + contractName, + codeBody, + }; + } else { + return { + type: StacksMessageType.Payload, + payloadType: PayloadType.SmartContract, + contractName, + codeBody, + }; + } } export interface PoisonPayload { @@ -172,6 +192,11 @@ export function serializePayload(payload: PayloadInput): Uint8Array { bytesArray.push(serializeStacksMessage(payload.contractName)); bytesArray.push(serializeStacksMessage(payload.codeBody)); break; + case PayloadType.VersionedSmartContract: + bytesArray.push(payload.clarityVersion); + bytesArray.push(serializeStacksMessage(payload.contractName)); + bytesArray.push(serializeStacksMessage(payload.codeBody)); + break; case PayloadType.PoisonMicroblock: // TODO: implement break; @@ -214,6 +239,15 @@ export function deserializePayload(bytesReader: BytesReader): Payload { const smartContractName = deserializeLPString(bytesReader); const codeBody = deserializeLPString(bytesReader, 4, 100_000); return createSmartContractPayload(smartContractName, codeBody); + + case PayloadType.VersionedSmartContract: { + const clarityVersion = bytesReader.readUInt8Enum(ClarityVersion, n => { + throw new Error(`Cannot recognize ClarityVersion: ${n}`); + }); + const smartContractName = deserializeLPString(bytesReader); + const codeBody = deserializeLPString(bytesReader, 4, 100_000); + return createSmartContractPayload(smartContractName, codeBody, clarityVersion); + } case PayloadType.PoisonMicroblock: // TODO: implement return createPoisonPayload(); diff --git a/packages/transactions/src/transaction.ts b/packages/transactions/src/transaction.ts index 316d996188..7d3e65d187 100644 --- a/packages/transactions/src/transaction.ts +++ b/packages/transactions/src/transaction.ts @@ -89,6 +89,7 @@ export class StacksTransaction { } case PayloadType.ContractCall: case PayloadType.SmartContract: + case PayloadType.VersionedSmartContract: case PayloadType.TokenTransfer: { this.anchorMode = AnchorMode.Any; break; diff --git a/packages/transactions/tests/builder.test.ts b/packages/transactions/tests/builder.test.ts index 9bcbb1e0d8..e2cefa7d6f 100644 --- a/packages/transactions/tests/builder.test.ts +++ b/packages/transactions/tests/builder.test.ts @@ -48,6 +48,7 @@ import { AddressHashMode, AnchorMode, AuthType, + ClarityVersion, DEFAULT_CORE_NODE_API_URL, FungibleConditionCode, NonFungibleConditionCode, @@ -656,6 +657,32 @@ test('addSignature to an unsigned transaction', async () => { expect(unsignedTx).not.toBe(signedTx); }); +test('Make versioned smart contract deploy', async () => { + const contractName = 'kv-store'; + const codeBody = fs.readFileSync('./tests/contracts/kv-store.clar').toString(); + const senderKey = 'e494f188c2d35887531ba474c433b1e41fadd8eb824aca983447fd4bb8b277a801'; + const fee = 0; + const nonce = 0; + + const transaction = await makeContractDeploy({ + contractName, + codeBody, + senderKey, + fee, + nonce, + network: new StacksTestnet(), + anchorMode: AnchorMode.Any, + clarityVersion: ClarityVersion.Clarity2, + }); + + const serialized = transaction.serialize().toString('hex'); + + const tx = + '80800000000400e6c05355e0c990ffad19a5e9bda394a9c50034290000000000000000000000000000000000009172c9841e763c32e827c177491f5228956e6ef1071043be898bfdd694bf3e680309b0666e8fec013a8a453573a8bd707152c9f21aa6f2d5e57c407af672b6f00302000000000602086b762d73746f72650000015628646566696e652d6d61702073746f72652028286b657920286275666620333229292920282876616c7565202862756666203332292929290a0a28646566696e652d7075626c696320286765742d76616c756520286b65792028627566662033322929290a20202020286d6174636820286d61702d6765743f2073746f72652028286b6579206b65792929290a2020202020202020656e74727920286f6b20286765742076616c756520656e74727929290a20202020202020202865727220302929290a0a28646566696e652d7075626c696320287365742d76616c756520286b65792028627566662033322929202876616c75652028627566662033322929290a2020202028626567696e0a2020202020202020286d61702d7365742073746f72652028286b6579206b6579292920282876616c75652076616c75652929290a2020202020202020286f6b2027747275652929290a'; + + expect(serialized).toBe(tx); +}); + test('Make smart contract deploy', async () => { const contractName = 'kv-store'; const codeBody = fs.readFileSync('./tests/contracts/kv-store.clar').toString(); diff --git a/packages/transactions/tests/payload.test.ts b/packages/transactions/tests/payload.test.ts index 6de86f0087..45ed1c323d 100644 --- a/packages/transactions/tests/payload.test.ts +++ b/packages/transactions/tests/payload.test.ts @@ -1,21 +1,22 @@ import { - TokenTransferPayload, - ContractCallPayload, - SmartContractPayload, CoinbasePayload, - createSmartContractPayload, + ContractCallPayload, createCoinbasePayload, createContractCallPayload, + createSmartContractPayload, createTokenTransferPayload, + SmartContractPayload, + TokenTransferPayload, + VersionedSmartContractPayload, } from '../src/payload'; import { serializeDeserialize } from './macros'; -import { trueCV, falseCV, standardPrincipalCV, contractPrincipalCV } from '../src/clarity'; +import { contractPrincipalCV, falseCV, standardPrincipalCV, trueCV } from '../src/clarity'; -import { COINBASE_BYTES_LENGTH, StacksMessageType } from '../src/constants'; -import { principalToString } from '../src/clarity/types/principalCV'; import { bytesToUtf8 } from '@stacks/common'; +import { principalToString } from '../src/clarity/types/principalCV'; +import { ClarityVersion, COINBASE_BYTES_LENGTH, StacksMessageType } from '../src/constants'; test('STX token transfer payload serialization and deserialization', () => { const recipient = standardPrincipalCV('SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159'); @@ -118,6 +119,30 @@ test('Smart contract payload serialization and deserialization', () => { expect(deserialized.codeBody.content).toBe(codeBody); }); +test('Versioned smart contract payload serialization and deserialization', () => { + const contractName = 'contract_name'; + const codeBody = + '(define-map store ((key (buff 32))) ((value (buff 32))))' + + '(define-public (get-value (key (buff 32)))' + + ' (match (map-get? store ((key key)))' + + ' entry (ok (get value entry))' + + ' (err 0)))' + + '(define-public (set-value (key (buff 32)) (value (buff 32)))' + + ' (begin' + + ' (map-set store ((key key)) ((value value)))' + + " (ok 'true)))"; + + const payload = createSmartContractPayload(contractName, codeBody, ClarityVersion.Clarity2); + + const deserialized = serializeDeserialize( + payload, + StacksMessageType.Payload + ) as VersionedSmartContractPayload; + expect(deserialized.clarityVersion).toBe(2); + expect(deserialized.contractName.content).toBe(contractName); + expect(deserialized.codeBody.content).toBe(codeBody); +}); + test('Coinbase payload serialization and deserialization', () => { // eslint-disable-next-line node/prefer-global/buffer const coinbaseBuffer = Buffer.alloc(COINBASE_BYTES_LENGTH, 0);