Skip to content

Commit

Permalink
feat: adds transferMultipleSpore and meltMultipleThenCreateSpore,…
Browse files Browse the repository at this point in the history
… with its tests
  • Loading branch information
code-monad committed Jul 25, 2024
1 parent 6b2589c commit 7f25b9f
Show file tree
Hide file tree
Showing 7 changed files with 599 additions and 17 deletions.
94 changes: 94 additions & 0 deletions packages/core/src/__tests__/Multiple.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { BI } from '@ckb-lumos/lumos';
import { describe, it } from 'vitest';
import { createMultipleSpores, getSporeById, meltMultipleThenCreateSpore, transferMultipleSpore } from '../api';
import { predefinedSporeConfigs } from '../config';
import { bytifyRawString } from '../helpers';
import { signAndOrSendTransaction } from './helpers';
import { MultipleTestSPORE_OUTPOINT_RECORDS, TEST_ACCOUNTS, TEST_ENV } from './shared';

const options = {
timeout: 10000000,
};
describe('Multiple', options, () => {
const { rpc, config } = TEST_ENV;
const { ALICE, BOB, CHARLIE } = TEST_ACCOUNTS;

it('Create Multiple First', async () => {
const createAmount = 2;
const { txSkeleton, outputIndices } = await createMultipleSpores({
sporeInfos: Array(createAmount).fill({
data: {
contentType: 'text/plain',
content: bytifyRawString('content'),
},
toLock: CHARLIE.lock,
}),
fromInfos: [CHARLIE.address],
config,
});
const { hash } = await signAndOrSendTransaction({
account: CHARLIE,
txSkeleton,
config,
rpc,
send: true,
});

if (hash) {
for (const index of outputIndices) {
MultipleTestSPORE_OUTPOINT_RECORDS.push({
outPoint: {
txHash: hash,
index: BI.from(index).toHexString(),
},
account: CHARLIE,
sporeId: txSkeleton.get('outputs').get(index)!.cellOutput.type!.args,
});
}
}
});
it('Multiple Transfer', async () => {
// wait for transaction success
// dirty but works
await new Promise((f) => setTimeout(f, 20000));
const spore_cells = MultipleTestSPORE_OUTPOINT_RECORDS.map((spore) => spore.outPoint);
const txSkeleton = await transferMultipleSpore({
outPoints: spore_cells,
fromInfos: [CHARLIE.address],
toLock: ALICE.lock,
config: predefinedSporeConfigs.Testnet,
});

const hash = await signAndOrSendTransaction({ account: CHARLIE, txSkeleton, config, rpc, send: true });
console.log(`Spore Multiple Transfer at: https://pudge.explorer.nervos.org/transaction/${hash.hash}`);
//console.log(`Spore ID: ${txSkeleton.get('outputs').get(outputIndex)!.cellOutput.type!.args}`);
}),
it('Multiple Melt Then Create One', async () => {
// wait for transaction success
// dirty but works
await new Promise((f) => setTimeout(f, 20000));
const sporeIds = MultipleTestSPORE_OUTPOINT_RECORDS.map((spore) => spore.sporeId);
const sporeCells = (
await Promise.all(
sporeIds.map(async (spore_id) => {
const sporeData = await getSporeById(spore_id, predefinedSporeConfigs.Testnet);
return sporeData?.outPoint;
}),
)
).filter((outPoint) => outPoint !== undefined);

const { txSkeleton } = await meltMultipleThenCreateSpore({
outPoints: sporeCells,
fromInfos: [ALICE.address],
toLock: BOB.lock,
config: predefinedSporeConfigs.Testnet,
data: {
contentType: 'text/plain',
content: bytifyRawString('content'),
},
});
const hash = await signAndOrSendTransaction({ account: ALICE, txSkeleton, config, rpc, send: true });
console.log(`Spore created at: https://pudge.explorer.nervos.org/transaction/${hash.hash}`);
//console.log(`Spore ID: ${txSkeleton.get('outputs').get(outputIndex)!.cellOutput.type!.args}`);
});
});
1 change: 1 addition & 0 deletions packages/core/src/__tests__/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './check';
export * from './retry';
export * from './file';
export * from './config';
export * from './wallet';
92 changes: 92 additions & 0 deletions packages/core/src/__tests__/helpers/wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { defaultEmptyWitnessArgs, updateWitnessArgs, isScriptValueEquals, getSporeConfig } from '../..';
import { hd, helpers, RPC, Address, Hash, Script, HexString } from '@ckb-lumos/lumos';
import { secp256k1Blake160 } from '@ckb-lumos/lumos/common-scripts';

export interface Wallet {
lock: Script;
address: Address;
signMessage(message: HexString): Hash;
signTransaction(txSkeleton: helpers.TransactionSkeletonType): helpers.TransactionSkeletonType;
signAndSendTransaction(txSkeleton: helpers.TransactionSkeletonType): Promise<Hash>;
}

/**
* Create a CKB Default Lock (Secp256k1Blake160 Sign-all) Wallet by a private-key and a SporeConfig,
* providing lock/address, and functions to sign message/transaction and send the transaction on-chain.
*/
export function createDefaultLockWallet(privateKey: HexString): Wallet {
const config = getSporeConfig();

// Generate a lock script from the private key
const defaultLock = config.lumos.SCRIPTS.SECP256K1_BLAKE160!;
const lock: Script = {
codeHash: defaultLock.CODE_HASH,
hashType: defaultLock.HASH_TYPE,
args: hd.key.privateKeyToBlake160(privateKey),
};

// Generate address from the lock script
const address = helpers.encodeToAddress(lock, {
config: config.lumos,
});

// Sign for a message
function signMessage(message: HexString): Hash {
return hd.key.signRecoverable(message, privateKey);
}

// Sign prepared signing entries,
// and then fill signatures into Transaction.witnesses
function signTransaction(txSkeleton: helpers.TransactionSkeletonType): helpers.TransactionSkeletonType {
const signingEntries = txSkeleton.get('signingEntries');
const signatures = new Map<HexString, Hash>();
const inputs = txSkeleton.get('inputs');

let witnesses = txSkeleton.get('witnesses');
for (let i = 0; i < signingEntries.size; i++) {
const entry = signingEntries.get(i)!;
if (entry.type === 'witness_args_lock') {
// Skip if the input's lock does not match to the wallet's lock
const input = inputs.get(entry.index);
if (!input || !isScriptValueEquals(input.cellOutput.lock, lock)) {
continue;
}

// Sign message
if (!signatures.has(entry.message)) {
const sig = signMessage(entry.message);
signatures.set(entry.message, sig);
}

// Update signature to Transaction.witnesses
const signature = signatures.get(entry.message)!;
const witness = witnesses.get(entry.index, defaultEmptyWitnessArgs);
witnesses = witnesses.set(entry.index, updateWitnessArgs(witness, 'lock', signature));
}
}

return txSkeleton.set('witnesses', witnesses);
}

// Sign the transaction and send it via RPC
async function signAndSendTransaction(txSkeleton: helpers.TransactionSkeletonType): Promise<Hash> {
// 1. Sign transaction
txSkeleton = secp256k1Blake160.prepareSigningEntries(txSkeleton, { config: config.lumos });
txSkeleton = signTransaction(txSkeleton);

// 2. Convert TransactionSkeleton to Transaction
const tx = helpers.createTransactionFromSkeleton(txSkeleton);

// 3. Send transaction
const rpc = new RPC(config.ckbNodeUrl);
return await rpc.sendTransaction(tx, 'passthrough');
}

return {
lock,
address,
signMessage,
signTransaction,
signAndSendTransaction,
};
}
6 changes: 6 additions & 0 deletions packages/core/src/__tests__/shared/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const CLUSTER_OUTPOINT_RECORDS: OutPointRecord[] = [];
export const CLUSTER_PROXY_OUTPOINT_RECORDS: OutPointRecord[] = [];
export const CLUSTER_AGENT_OUTPOINT_RECORDS: OutPointRecord[] = [];

export interface OutpointWithSporeIdRecord extends OutPointRecord {
sporeId: string;
}

export const MultipleTestSPORE_OUTPOINT_RECORDS: OutpointWithSporeIdRecord[] = [];

export async function cleanupRecords(props: { name: string }) {
const [sporeCleanupResults, clusterProxyCleanupResults, clusterAgentCleanupResults] = await Promise.all([
cleanupSporeRecords(),
Expand Down
Loading

0 comments on commit 7f25b9f

Please sign in to comment.