Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/target
proptest-regressions/
.cargo-root/
node_modules/
pnpm-lock.yaml
generated/
29 changes: 29 additions & 0 deletions programs/settlement/idl/client/js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "js-client",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"files": [
"./dist/src",
"./dist/types",
"./src/"
],
"scripts": {
"test": "vitest run"
},
"keywords": [],
"author": "",
"peerDependencies": {
"@solana/kit": "^6.10.0"
},
"dependencies": {
"@solana/program-client-core": "^6.10.0"
},
"devDependencies": {
"@solana/kit": "^6.10.0",
"@types/node": "^24.10.0",
"litesvm": "^1.3.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
}
}
33 changes: 33 additions & 0 deletions programs/settlement/idl/client/js/src/hooked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Hand-written — Codama's "hooked" convention: functions referenced by a
// `resolverValueNode` default value live here, imported by generated code
// via the fixed "../../hooked" specifier (see generate.mjs). Never touched
// by rendering, which only manages src/generated.
//
// create_order's order PDA is seeded by ["settlement", sha256(borsh(intent)), "order"].
// The middle seed is a hash of the whole `intent` argument rather than a plain
// field/account reference, which the Anchor PDA-seed grammar (const / arg / account)
// can't express — see cow_settlement.json's create_order docs — so it's handed off
// to this resolver instead of a plain `pda` node. Codama calls this with a
// `resolverScope` object and splices the returned `{ value }` onto the account
// it's resolving, so its shape is dictated by the renderer, not chosen here.

import { getProgramDerivedAddress, type Address } from "@solana/kit";
import { getOrderIntentEncoder, type OrderIntentArgs } from "./generated";

export async function resolveOrderPda({
programAddress,
args,
}: {
programAddress: Address;
args: { intent: OrderIntentArgs };
}): Promise<{ value: Address }> {
const intentBytes = getOrderIntentEncoder().encode(args.intent);
const orderUid = new Uint8Array(
await crypto.subtle.digest("SHA-256", intentBytes),
);
const [address] = await getProgramDerivedAddress({
programAddress,
seeds: ["settlement", orderUid, "order"],
});
return { value: address };
}
1 change: 1 addition & 0 deletions programs/settlement/idl/client/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./generated";
75 changes: 75 additions & 0 deletions programs/settlement/idl/client/js/tests/createOrder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { LiteSVM } from "litesvm";
import { beforeEach, describe, expect, it } from "vitest";
import {
appendTransactionMessageInstruction,
createTransactionMessage,
generateKeyPairSigner,
lamports,
pipe,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
} from "@solana/kit";
import {
COW_SETTLEMENT_PROGRAM_ADDRESS,
getCreateOrderInstructionAsync,
getOrderAccountDecoder,
} from "../src/generated";
import { resolveOrderPda } from "../src/hooked";
import { buildOrderIntent, COW_SETTLEMENT_SO_PATH } from "./fixtures";

describe("createOrder", () => {
let svm: LiteSVM;

beforeEach(() => {
svm = new LiteSVM();
svm.addProgramFromFile(COW_SETTLEMENT_PROGRAM_ADDRESS, COW_SETTLEMENT_SO_PATH);
});

it("creates an order account matching the submitted intent", async () => {
const owner = await generateKeyPairSigner();
svm.airdrop(owner.address, lamports(1_000_000_000n));

const intent = await buildOrderIntent({ owner: owner.address });
const instruction = await getCreateOrderInstructionAsync({
owner,
createdBy: owner,
intent,
});

const tx = await pipe(
createTransactionMessage({ version: 0 }),
(t) => setTransactionMessageFeePayerSigner(owner, t),
(t) =>
setTransactionMessageLifetimeUsingBlockhash(
{ blockhash: svm.latestBlockhash(), lastValidBlockHeight: 999_999_999n },
t,
),
(t) => appendTransactionMessageInstruction(instruction, t),
signTransactionMessageWithSigners,
);

const result = svm.sendTransaction(tx);
if ("err" in result) {
throw new Error(`createOrder failed: ${JSON.stringify(result.err)}`);
}

const { value: orderPda } = await resolveOrderPda({
programAddress: COW_SETTLEMENT_PROGRAM_ADDRESS,
args: { intent },
});
const account = svm.getAccount(orderPda);
expect(account).not.toBeNull();

const decoded = getOrderAccountDecoder().decode(account!.data);
expect(decoded.cancelled).toBe(false);
expect(decoded.amountWithdrawn).toBe(0n);
expect(decoded.amountReceived).toBe(0n);
expect(decoded.createdBy).toBe(owner.address);
expect(decoded.intent).toEqual({
...intent,
sellAmount: BigInt(intent.sellAmount),
buyAmount: BigInt(intent.buyAmount),
});
});
});
26 changes: 26 additions & 0 deletions programs/settlement/idl/client/js/tests/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import path from "node:path";
import { generateKeyPairSigner, type Address } from "@solana/kit";
import { OrderKind, type OrderIntentArgs } from "../src/generated";

export const COW_SETTLEMENT_SO_PATH = path.join(
import.meta.dirname,
"../../../../../../target/deploy/cow_settlement.so",
);

export async function buildOrderIntent(
overrides: Partial<OrderIntentArgs> & { owner: Address },
): Promise<OrderIntentArgs> {
const buyTokenAccount = await generateKeyPairSigner();
const sellTokenAccount = await generateKeyPairSigner();
return {
buyTokenAccount: buyTokenAccount.address,
sellTokenAccount: sellTokenAccount.address,
sellAmount: 1_000_000n,
buyAmount: 2_000_000n,
validTo: Math.floor(Date.now() / 1000) + 3600,
kind: OrderKind.Sell,
partiallyFillable: false,
appData: new Uint8Array(32),
...overrides,
};
}
12 changes: 12 additions & 0 deletions programs/settlement/idl/client/js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"types": ["node"]
},
"include": ["src", "tests"]
}
34 changes: 34 additions & 0 deletions programs/settlement/idl/generate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
createFromRoot,
resolverValueNode,
argumentValueNode,
setInstructionAccountDefaultValuesVisitor,
} from "codama";
import { rootNodeFromAnchor } from "@codama/nodes-from-anchor";
import { renderVisitor } from "@codama/renderers-js";
import IDL from "./cow_settlement.json" with {type: 'json'};

const codama = createFromRoot(rootNodeFromAnchor(IDL));

// order_pda seed generation requires hashing the input intent in `createOrder`
// so we use codama's `resolverValueNode` to inject custom code for this.
codama.update(
setInstructionAccountDefaultValuesVisitor([
{
instruction: "createOrder",
account: "orderPda",
defaultValue: resolverValueNode("resolveOrderPda", {
dependsOn: [argumentValueNode("intent")],
}),
},
]),
);

codama.accept(
renderVisitor("./client/js", {
// resolveOrderPda hashes with crypto.subtle.digest, which is async, so
// codama must await it and only wire it into the *Async instruction
// builder rather than the sync one.
asyncResolvers: ["resolveOrderPda"],
}),
);
18 changes: 18 additions & 0 deletions programs/settlement/idl/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "idl",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.30.3",
"dependencies": {
"@codama/nodes-from-anchor": "^1.5.2",
"@codama/renderers-js": "^2.3.0",
"codama": "^1.9.0"
}
}