diff --git a/src/lib/engine/engine.ts b/src/lib/engine/engine.ts index e48eebbd..5ef81be7 100644 --- a/src/lib/engine/engine.ts +++ b/src/lib/engine/engine.ts @@ -1,6 +1,7 @@ export * from './types/bcmr-types.js'; // export * from './types/draft-transaction-workspace-types.js'; export * from './types/template-types.js'; +export * from './parse-nft.js'; /* * export * from './types/wallet-types.js'; * export * from './types/wallet-activity-types.js'; diff --git a/src/lib/engine/parse-nft.spec.ts b/src/lib/engine/parse-nft.spec.ts new file mode 100644 index 00000000..b380e878 --- /dev/null +++ b/src/lib/engine/parse-nft.spec.ts @@ -0,0 +1,76 @@ +import test from 'ava'; + +import { + binToHex, + createVirtualMachineBCH, + hexToBin, + NonFungibleTokenCapability, + parseNft, +} from '../lib.js'; + +test('parseNft: returns the expected value of altstack', (t) => { + // Example parsable NFT from https://www.bitcoincashsite.com/blog/token-pioneers-cashtokens-tutorial-4/ + const nft1 = { + lockingBytecode: hexToBin(''), + token: { + amount: BigInt(0), + category: hexToBin(''), + nft: { + capability: NonFungibleTokenCapability.none, + commitment: hexToBin( + '30313130313031303131323032333132313231393030313034333030303030', + ), + }, + }, + valueSatoshis: BigInt(1000), + }; + const bytecode = + '00cf527f780230348763786b587f786b6b67780230378763786b5c7f786b6b67786b587f786b5c7f786b6b7568687575'; + const nft1AltStack: Uint8Array[] = parseNft(nft1, bytecode); + t.is(binToHex(nft1AltStack[0] ?? new Uint8Array()), '3031'); + t.is(binToHex(nft1AltStack[1] ?? new Uint8Array()), '3130313031303131'); + t.is( + binToHex(nft1AltStack[2] ?? new Uint8Array()), + '323032333132313231393030', + ); + t.is(binToHex(nft1AltStack[3] ?? new Uint8Array()), '313034333030303030'); +}); + +test('parseNft: bottom of altstack contains the commitment if bytecode is 00cf6b', (t) => { + // Example from bcmr-v2.schema.ts + const nft2 = { + lockingBytecode: hexToBin(''), + token: { + amount: BigInt(0), + category: hexToBin(''), + nft: { + capability: NonFungibleTokenCapability.none, + commitment: hexToBin('2021'), + }, + }, + valueSatoshis: BigInt(1000), + }; + const bytecode = '00cf6b'; + const nft2AltStack: Uint8Array[] = parseNft(nft2, bytecode); + t.is(binToHex(nft2AltStack[0] ?? new Uint8Array()), '2021'); +}); + +test('parseNft: works with compatible vm', (t) => { + // Example from bcmr-v2.schema.ts + const nft2 = { + lockingBytecode: hexToBin(''), + token: { + amount: BigInt(0), + category: hexToBin(''), + nft: { + capability: NonFungibleTokenCapability.none, + commitment: hexToBin('2021'), + }, + }, + valueSatoshis: BigInt(1000), + }; + const bytecode = '00cf6b'; + const vmCreator = () => createVirtualMachineBCH(); + const nft2AltStack: Uint8Array[] = parseNft(nft2, bytecode, vmCreator); + t.is(binToHex(nft2AltStack[0] ?? new Uint8Array()), '2021'); +}); diff --git a/src/lib/engine/parse-nft.ts b/src/lib/engine/parse-nft.ts new file mode 100644 index 00000000..f9a232c3 --- /dev/null +++ b/src/lib/engine/parse-nft.ts @@ -0,0 +1,69 @@ +import { hexToBin } from '../format/format.js'; +import type { + AuthenticationProgramCommon, + AuthenticationProgramStateCommon, + AuthenticationVirtualMachine, + Output, + ResolvedTransactionCommon, +} from '../lib.js'; +import { createVirtualMachineBCHCHIPs } from '../vm/instruction-sets/bch/chips/bch-chips-vm.js'; + +export type VMCreator = () => AuthenticationVirtualMachine< + ResolvedTransactionCommon, + AuthenticationProgramCommon, + AuthenticationProgramStateCommon +>; + +/** + * Returns the altstack as a result of parsing the NFT's commitment using the + * provided bytecode. + * + * @param utxo - the NFT to parse + * @param bytecode - the bytecode as hex string + * @param createVirtualMachine - a function that returns an {@link AuthenticationVirtualMachine} + */ +export const parseNft = ( + utxo: Output, + bytecode: string, + createVirtualMachine?: VMCreator, +): Uint8Array[] => { + const vm = createVirtualMachine + ? createVirtualMachine() + : createVirtualMachineBCHCHIPs(); + + const { alternateStack } = vm.evaluate({ + inputIndex: 1, + sourceOutputs: [ + utxo, + { + lockingBytecode: hexToBin(bytecode), + valueSatoshis: BigInt(0), + }, + ], + transaction: { + inputs: [ + { + outpointIndex: 0, + outpointTransactionHash: hexToBin(''), + sequenceNumber: 0, + unlockingBytecode: hexToBin(''), + }, + { + outpointIndex: 0, + outpointTransactionHash: hexToBin(''), + sequenceNumber: 0, + unlockingBytecode: hexToBin('51'), + }, + ], + locktime: 0, + outputs: [ + { + lockingBytecode: hexToBin('6a'), + valueSatoshis: BigInt(0), + }, + ], + version: 2, + }, + }); + return alternateStack; +};