diff --git a/contracts-c/display/display.c b/contracts-c/display/display.c new file mode 100644 index 0000000..ad8c7e0 --- /dev/null +++ b/contracts-c/display/display.c @@ -0,0 +1,135 @@ +#include "hookapi.h" + +#define SVAR(x) &(x), sizeof(x) + +#define NOPE(x) \ + { \ + return rollback((x), sizeof(x), __LINE__); \ + } + +#define MULTIPLIER 0.2 // 0.2 (6 XAH for 30 LEDGERS == 0.2 XAH PER LEDGER) +#define MAX 60 // 30 LEDGERS + +uint8_t bids_ns[32] = { + 0xAF, 0xD5, 0x20, 0x13, 0x12, 0xCC, 0xC1, 0xA5, + 0x41, 0x02, 0x63, 0x25, 0x9D, 0x0B, 0x22, 0x30, + 0x99, 0x18, 0x52, 0x16, 0x95, 0x5F, 0x8F, 0x6E, + 0x1C, 0x71, 0xF4, 0x14, 0xF0, 0x32, 0x48, 0xB6}; +; + +int64_t hook(uint32_t r) +{ + _g(1, 1); + // ACCOUNT: Origin Tx Account + uint8_t otxn_accid[32]; + otxn_field(otxn_accid + 12, 20, sfAccount); + + // ACCOUNT: Hook Account + uint8_t hook_accid[32]; + hook_account(hook_accid + 12, 20); + + // FILTER ON: ACCOUNT + if (BUFFER_EQUAL_20(hook_accid + 12, otxn_accid + 12)) + DONE("display.c: outgoing tx on `Account`."); + + int64_t tt = otxn_type(); + if (tt != ttINVOKE && tt != ttPAYMENT) + NOPE("display.c: Rejecting non-Invoke, non-Payment txn."); + + uint8_t amount_buffer[48]; + otxn_slot(1); + slot_subfield(1, sfAmount, 2); + + int64_t amount_xfl; + uint32_t flags; + if (tt == ttPAYMENT) + { + // this will fail if flags isn't in the txn, that's also ok. + otxn_field(&flags, 4, sfFlags); + + // check for partial payments (0x00020000) -> (0x00000200 LE) + if (flags & 0x200U) + NOPE("display.c: Partial payments are not supported."); + + otxn_field(SBUF(amount_buffer), sfAmount); + + amount_xfl = slot_float(2); + + if (amount_xfl < 0 || !float_compare(amount_xfl, 1, COMPARE_GREATER)) + NOPE("display.c: Invalid sfAmount."); + } + + // // Operation + uint8_t op; + if (otxn_param(&op, 1, "OP", 2) != 1) + NOPE("display.c: Missing OP parameter on Invoke/Payment."); + + // sanity check + if (op == 'B' && tt != ttPAYMENT) + NOPE("display.c: Bid operations must be a payment transaction."); + + // sanity check + if (op == 'U' && tt != ttINVOKE) + NOPE("display.c: Update operations must be a invoke transaction."); + + int64_t count; + if (state(&count, 8, hook_accid + 12, 20) == DOESNT_EXIST) + { + // set current if initialization + ASSERT(0 < state_set(&count, 8, hook_accid + 12, 20)); + } + + switch (op) + { + case 'B': + { + uint8_t tid[40]; + if (otxn_param(tid, 32, "ID", 2) != 32) + NOPE("display.c: Missing ID parameter on Payment."); + + int64_t bid_count; + state_foreign(&bid_count, 8, hook_accid + 12, 20, bids_ns, 32, hook_accid + 12, 20); + + int64_t end_ledger; + int64_t cur_ledger = ledger_seq(); + if (state_foreign(&end_ledger, 8, hook_accid + 12, 20, 0, 32, hook_accid + 12, 20) == DOESNT_EXIST || end_ledger < cur_ledger) + { + end_ledger = cur_ledger; + } + + // calculate ledgers + int64_t amount_int = float_int(amount_xfl, 0, 1); + int64_t duration = amount_int / MULTIPLIER; + if (duration > MAX) + NOPE("display.c: Duration cannot be more than 60 ledgers."); + + end_ledger += duration; + UINT64_TO_BUF(tid + 32, end_ledger); + ASSERT(0 < state_foreign_set(tid, 40, &bid_count, 8, bids_ns, 32, hook_accid + 12, 20)); + + bid_count++; + ASSERT(0 < state_foreign_set(&bid_count, 8, hook_accid + 12, 20, bids_ns, 32, hook_accid + 12, 20)); + ASSERT(0 < state_foreign_set(&end_ledger, 8, hook_accid + 12, 20, 0, 32, hook_accid + 12, 20)); + TRACESTR("display.c: Bid."); + return accept(SBUF("display.c: Bid."), __LINE__); + } + case 'U': + { + uint8_t tid[40]; + state_foreign(tid, 40, &count, 8, bids_ns, 32, hook_accid + 12, 20); + uint64_t final_ledger = UINT64_FROM_BUF(tid + 32); + int64_t cur_sequence = ledger_seq(); + if (cur_sequence < final_ledger) + { + NOPE("display.c: Duration has not expired."); + } + + state_foreign_set(0, 0, &count, 8, bids_ns, 32, hook_accid + 12, 20); + + count++; + ASSERT(0 < state_set(&count, 8, hook_accid + 12, 20)); + TRACESTR("display.c: Update."); + return accept(SBUF("display.c: Update."), __LINE__); + } + } +} \ No newline at end of file diff --git a/test/integration-c/display/display.test.ts b/test/integration-c/display/display.test.ts new file mode 100644 index 0000000..03385e6 --- /dev/null +++ b/test/integration-c/display/display.test.ts @@ -0,0 +1,166 @@ +// xrpl +import { + URITokenMint, + Payment, + Invoke, + SetHookFlags, + convertStringToHex, + TransactionMetadata, + xrpToDrops, +} from '@transia/xrpl' +// xrpl-helpers +import { + XrplIntegrationTestContext, + setupClient, + teardownClient, + serverUrl, + close, +} from '@transia/hooks-toolkit/dist/npm/src/libs/xrpl-helpers' +// src +import { + SetHookParams, + createHookPayload, + setHooksV3, + iHookParamEntry, + iHookParamName, + iHookParamValue, + ExecutionUtility, + Xrpld, + StateUtility, + hexNamespace, + padHexString, + flipHex, +} from '@transia/hooks-toolkit/dist/npm/src' +import { hashURIToken } from '@transia/xrpl/dist/npm/utils/hashes' +import { + decodeModel, + hexToUInt64, + xrpAddressToHex, +} from '@transia/hooks-toolkit/dist/npm/src/libs/binary-models' +import { DisplayModel } from './models/DisplayModel' + +// LevelThree: ACCEPT: success + +describe('hunt', () => { + let testContext: XrplIntegrationTestContext + + beforeAll(async () => { + testContext = await setupClient(serverUrl) + const hookWallet = testContext.hook1 + + const hook1 = createHookPayload({ + version: 0, + createFile: 'display', + namespace: 'display', + flags: SetHookFlags.hsfOverride, + hookOnArray: ['Payment', 'Invoke'], + }) + await setHooksV3({ + client: testContext.client, + seed: hookWallet.seed, + hooks: [{ Hook: hook1 }], + } as SetHookParams) + }) + afterAll(async () => teardownClient(testContext)) + + it('display - success', async () => { + const aliceWallet = testContext.alice + const hookWallet = testContext.hook1 + + // Bob: Mint and Sell + const random = Math.random() + const builtTx1: URITokenMint = { + TransactionType: 'URITokenMint', + Account: aliceWallet.classicAddress, + URI: convertStringToHex(`ipfs://${random}`), + } + await Xrpld.submit(testContext.client, { + wallet: aliceWallet, + tx: builtTx1, + }) + await close(testContext.client) + const uriTokenID = hashURIToken( + aliceWallet.classicAddress, + `ipfs://${random}` + ) + + console.log(uriTokenID) + + const param1 = new iHookParamEntry( + new iHookParamName('OP'), + new iHookParamValue('B') + ) + const param2 = new iHookParamEntry( + new iHookParamName('ID'), + new iHookParamValue( + '35D1168C6D5B107A3B884B055D0657E36196882AAA16C68E345AC998717E86B3', + true + ) + ) + const builtTx: Payment = { + TransactionType: 'Payment', + Account: aliceWallet.classicAddress, + Destination: hookWallet.classicAddress, + Amount: xrpToDrops('6'), + HookParameters: [param1.toXrpl(), param2.toXrpl()], + } + const result = await Xrpld.submit(testContext.client, { + wallet: aliceWallet, + tx: builtTx, + }) + const hookExecutions = await ExecutionUtility.getHookExecutionsFromMeta( + testContext.client, + result.meta as TransactionMetadata + ) + console.log(hookExecutions.executions[0].HookReturnString) + + await close(testContext.client) + + const currentState = await StateUtility.getHookState( + testContext.client, + testContext.hook1.classicAddress, + padHexString(xrpAddressToHex(testContext.hook1.classicAddress)), + hexNamespace('display') + ) + + console.log(hexToUInt64(flipHex(currentState.HookStateData))) + + console.log(padHexString(currentState.HookStateData, 64)) + console.log(hexNamespace('bids')) + + const state = await StateUtility.getHookState( + testContext.client, + testContext.hook1.classicAddress, + padHexString(currentState.HookStateData, 64), + hexNamespace('bids') + ) + + console.log(decodeModel(state.HookStateData, DisplayModel)) + + for (let index = 0; index < 30; index++) { + await close(testContext.client) + } + + const tx2Param1 = new iHookParamEntry( + new iHookParamName('OP'), + new iHookParamValue('U') + ) + const builtTx2: Invoke = { + TransactionType: 'Invoke', + Account: aliceWallet.classicAddress, + Destination: hookWallet.classicAddress, + HookParameters: [tx2Param1.toXrpl()], + } + const result2 = await Xrpld.submit(testContext.client, { + wallet: aliceWallet, + tx: builtTx2, + }) + const hookExecutions2 = await ExecutionUtility.getHookExecutionsFromMeta( + testContext.client, + result2.meta as TransactionMetadata + ) + console.log(hookExecutions2.executions[0].HookReturnString) + + await close(testContext.client) + }) +}) diff --git a/test/integration-c/display/models/DisplayModel.ts b/test/integration-c/display/models/DisplayModel.ts new file mode 100644 index 0000000..226a003 --- /dev/null +++ b/test/integration-c/display/models/DisplayModel.ts @@ -0,0 +1,31 @@ +import { + BaseModel, + Hash256, + Metadata, + UInt64, +} from '@transia/hooks-toolkit/dist/npm/src/libs/binary-models' + +export class DisplayModel extends BaseModel { + id: Hash256 + lastLedger: UInt64 + + constructor(id: Hash256, lastLedger: UInt64) { + super() + this.id = id + this.lastLedger = lastLedger + } + + getMetadata(): Metadata { + return [ + { field: 'id', type: 'hash256' }, + { field: 'lastLedger', type: 'uint64' }, + ] + } + + toJSON() { + return { + id: this.id, + lastLedger: this.lastLedger, + } + } +}