diff --git a/src/filler/handlers/simpleassets/processors/assets.integration.test.ts b/src/filler/handlers/simpleassets/processors/assets.integration.test.ts index f9f6d422..e5956597 100644 --- a/src/filler/handlers/simpleassets/processors/assets.integration.test.ts +++ b/src/filler/handlers/simpleassets/processors/assets.integration.test.ts @@ -408,6 +408,29 @@ describe('simpleassets assetProcessor', () => { }); }); + describe('transfer record', () => { + it('records the sender, the recipient, the memo cut to 256 characters and each asset when store_transfers is on', async () => { + register({ store_transfers: true }); + await seedAsset('600', { owner: 'alice' }); + await seedAsset('601', { owner: 'alice' }); + + await runBatch([{ + block: blockAt(6000), name: 'transfer', + data: { from: 'alice', to: 'bob', assetids: ['600', '601'], memo: 'm'.repeat(300) } + }]); + + const transfers = await client.query( + 'SELECT sender, recipient, memo FROM simpleassets_transfers WHERE contract = $1', [CONTRACT] + ); + expect(transfers.rows).to.deep.equal([{ sender: 'alice', recipient: 'bob', memo: 'm'.repeat(256) }]); + + const assets = await client.query( + 'SELECT asset_id, "index" FROM simpleassets_transfers_assets WHERE contract = $1 ORDER BY "index"', [CONTRACT] + ); + expect(assets.rows.map(row => [String(row.asset_id), Number(row.index)])).to.deep.equal([['600', 1], ['601', 2]]); + }); + }); + describe('catch-up write buffer', () => { it('gives consecutive ordinals across two create flushes in one transaction', async () => { await setTotal(GROUP_A, 10); diff --git a/src/filler/handlers/simpleassets/processors/assets.ts b/src/filler/handlers/simpleassets/processors/assets.ts index b816ab67..8f9a9bca 100644 --- a/src/filler/handlers/simpleassets/processors/assets.ts +++ b/src/filler/handlers/simpleassets/processors/assets.ts @@ -51,6 +51,11 @@ function compareCreates(a: PendingCreate, b: PendingCreate): number { return left < right ? -1 : (left > right ? 1 : 0); } +type AssetMatch = 'asset_id = $2' + | 'asset_id = ANY($2)' + | 'asset_id = ANY ($2)' + | 'asset_id = ANY ($2) AND owner = $3'; + export function assetProcessor(core: SimpleAssetsHandler, processor: DataProcessor): () => any { const destructors: Array<() => any> = []; const contract = core.args.simpleassets_account; @@ -63,6 +68,48 @@ export function assetProcessor(core: SimpleAssetsHandler, processor: DataProcess // create in the commit batch before any action that changes an asset. let pendingCreates: PendingCreate[] = []; + // The block guard keeps a replayed older action from overwriting a newer asset state. + // The match text starts after the contract condition ($1) and ends before the guard. Its + // parameters follow $1 in order, and the block number is the last parameter. The type lists + // each allowed text, so no caller can build one from action data. + async function updateAssetGuarded( + db: ContractDBTransaction, block: ShipBlock, fields: Record, match: AssetMatch, matchValues: any[] + ): Promise { + const blockTime = eosioTimestampToDate(block.timestamp).getTime(); + + await db.update('simpleassets_assets', { + ...fields, + updated_at_block: block.block_num, + updated_at_time: blockTime, + }, { + str: 'contract = $1 AND ' + match + ' AND updated_at_block <= $' + (matchValues.length + 2), + values: [contract, ...matchValues, block.block_num] + }, ['contract', 'asset_id']); + } + + async function insertTransferRecord( + db: ContractDBTransaction, block: ShipBlock, tx: EosioTransaction, trace: EosioActionTrace, + sender: string, recipient: string, memo: string, onConflict: 'update' | 'nothing' + ): Promise { + await db.insert('simpleassets_transfers', { + contract: contract, + transfer_id: trace.global_sequence, + sender, + recipient, + memo, + txid: Buffer.from(tx.id, 'hex'), + created_at_block: block.block_num, + created_at_time: eosioTimestampToDate(block.timestamp).getTime() + }, ['contract', 'transfer_id'], true, true, onConflict); + + await db.insert('simpleassets_transfers_assets', trace.act.data.assetids.map((assetID, index) => ({ + transfer_id: trace.global_sequence, + contract: contract, + index: index + 1, + asset_id: assetID + })), ['contract', 'transfer_id', 'asset_id'], true, true, 'update'); + } + async function readBaselineBlock(db: ContractDBTransaction): Promise { const query = await db.query( 'SELECT bootstrap_baseline_block FROM simpleassets_config WHERE contract = $1', @@ -215,80 +262,47 @@ export function assetProcessor(core: SimpleAssetsHandler, processor: DataProcess destructors.push(processor.onActionTrace( contract, 'burnlog', async (db: ContractDBTransaction, block: ShipBlock, tx: EosioTransaction, trace: EosioActionTrace): Promise => { - await db.update('simpleassets_assets', { + await updateAssetGuarded(db, block, { owner: null, burned_by_account: trace.act.data.owner, burned_at_block: block.block_num, burned_at_time: eosioTimestampToDate(block.timestamp).getTime(), - updated_at_block: block.block_num, - updated_at_time: eosioTimestampToDate(block.timestamp).getTime(), - }, { - str: 'contract = $1 AND asset_id = ANY($2) AND updated_at_block <= $3', - values: [contract, trace.act.data.assetids, block.block_num] - }, ['contract', 'asset_id']); + }, 'asset_id = ANY($2)', [trace.act.data.assetids]); }, SimpleAssetsUpdatePriority.ACTION_UPDATE_ASSET.valueOf() )); destructors.push(processor.onActionTrace( contract, 'update', async (db: ContractDBTransaction, block: ShipBlock, tx: EosioTransaction, trace: EosioActionTrace): Promise => { - await db.update('simpleassets_assets', { + await updateAssetGuarded(db, block, { mutable_data: encodeDatabaseJson(parseJsonObject(trace.act.data.mdata)), - updated_at_block: block.block_num, - updated_at_time: eosioTimestampToDate(block.timestamp).getTime(), - }, { - str: 'contract = $1 AND asset_id = $2 AND updated_at_block <= $3', - values: [contract, trace.act.data.assetid, block.block_num] - }, ['contract', 'asset_id']); + }, 'asset_id = $2', [trace.act.data.assetid]); }, SimpleAssetsUpdatePriority.ACTION_UPDATE_ASSET.valueOf() )); destructors.push(processor.onActionTrace( contract, 'changeauthor', async (db: ContractDBTransaction, block: ShipBlock, tx: EosioTransaction, trace: EosioActionTrace): Promise => { - await db.update('simpleassets_assets', { + await updateAssetGuarded(db, block, { author: trace.act.data.newauthor, - updated_at_block: block.block_num, - updated_at_time: eosioTimestampToDate(block.timestamp).getTime(), - }, { - str: 'contract = $1 AND asset_id = ANY($2) AND updated_at_block <= $3', - values: [contract, trace.act.data.assetids, block.block_num] - }, ['contract', 'asset_id']); + }, 'asset_id = ANY($2)', [trace.act.data.assetids]); }, SimpleAssetsUpdatePriority.ACTION_UPDATE_ASSET.valueOf() )); destructors.push(processor.onActionTrace( contract, 'transfer', async (db: ContractDBTransaction, block: ShipBlock, tx: EosioTransaction, trace: EosioActionTrace): Promise => { - await db.update('simpleassets_assets', { + await updateAssetGuarded(db, block, { owner: trace.act.data.to, transferred_at_block: block.block_num, transferred_at_time: eosioTimestampToDate(block.timestamp).getTime(), - updated_at_block: block.block_num, - updated_at_time: eosioTimestampToDate(block.timestamp).getTime(), - }, { - str: 'contract = $1 AND asset_id = ANY ($2) AND owner = $3 AND updated_at_block <= $4', - values: [contract, trace.act.data.assetids, trace.act.data.from, block.block_num] - }, ['contract', 'asset_id']); + }, 'asset_id = ANY ($2) AND owner = $3', [trace.act.data.assetids, trace.act.data.from]); if (core.args.store_transfers) { - await db.insert('simpleassets_transfers', { - contract: contract, - transfer_id: trace.global_sequence, - sender: trace.act.data.from, - recipient: trace.act.data.to, - memo: String(trace.act.data.memo).substr(0, 256), - txid: Buffer.from(tx.id, 'hex'), - created_at_block: block.block_num, - created_at_time: eosioTimestampToDate(block.timestamp).getTime() - }, ['contract', 'transfer_id'], true, true, 'update'); - - await db.insert('simpleassets_transfers_assets', trace.act.data.assetids.map((assetID, index) => ({ - transfer_id: trace.global_sequence, - contract: contract, - index: index + 1, - asset_id: assetID - })), ['contract', 'transfer_id', 'asset_id'], true, true, 'update'); + await insertTransferRecord( + db, block, tx, trace, + trace.act.data.from, trace.act.data.to, String(trace.act.data.memo).substr(0, 256), 'update' + ); } }, SimpleAssetsUpdatePriority.ACTION_UPDATE_ASSET.valueOf() )); @@ -303,36 +317,18 @@ export function assetProcessor(core: SimpleAssetsHandler, processor: DataProcess [contract, trace.act.data.assetids] ) : null; - await db.update('simpleassets_assets', { + await updateAssetGuarded(db, block, { owner: trace.act.data.claimer, transferred_at_block: block.block_num, transferred_at_time: eosioTimestampToDate(block.timestamp).getTime(), - updated_at_block: block.block_num, - updated_at_time: eosioTimestampToDate(block.timestamp).getTime(), - }, { - str: 'contract = $1 AND asset_id = ANY ($2) AND updated_at_block <= $3', - values: [contract, trace.act.data.assetids, block.block_num] - }, ['contract', 'asset_id']); + }, 'asset_id = ANY ($2)', [trace.act.data.assetids]); if (core.args.store_transfers) { // A replay reads the claimer as the owner, so the first write keeps the sender. - await db.insert('simpleassets_transfers', { - contract: contract, - transfer_id: trace.global_sequence, - sender: fromQuery.rowCount > 0 ? fromQuery.rows[0].owner : '.', - recipient: trace.act.data.claimer, - memo: '', - txid: Buffer.from(tx.id, 'hex'), - created_at_block: block.block_num, - created_at_time: eosioTimestampToDate(block.timestamp).getTime() - }, ['contract', 'transfer_id'], true, true, 'nothing'); - - await db.insert('simpleassets_transfers_assets', trace.act.data.assetids.map((assetID, index) => ({ - transfer_id: trace.global_sequence, - contract: contract, - index: index + 1, - asset_id: assetID - })), ['contract', 'transfer_id', 'asset_id'], true, true, 'update'); + await insertTransferRecord( + db, block, tx, trace, + fromQuery.rowCount > 0 ? fromQuery.rows[0].owner : '.', trace.act.data.claimer, '', 'nothing' + ); } }, SimpleAssetsUpdatePriority.ACTION_UPDATE_ASSET.valueOf() ));