Skip to content
Open
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
48 changes: 48 additions & 0 deletions examples/verify-anchored-payout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Keypair } from "@stellar/stellar-sdk";
import {
anchorFundablePayout,
verifyAnchoredPayoutReceipt,
type FundablePayoutAuditRecord,
} from "../src/index.js";

async function main() {
console.log("=== Nirium Audit Trail Anchor for Fundable Payouts ===\n");

const payer = Keypair.random();
const recipient = Keypair.random();

const samplePayout: FundablePayoutAuditRecord = {
protocol: "fundable",
chain: "stellar",
networkPassphrase: "Test SDF Network ; September 2015",
contractId: "CA3D5KRYMCMCZVAC7OHQHG2CAUAQ77PDRWNXD7BOD7X62F26T4ZMY54K",
operation: "create_flow",
txHash: "4b7b3b9b8c0a1e2f3d4c5b6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c",
sender: payer.publicKey(),
recipient: recipient.publicKey(),
token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
amountOrRate: "5000000",
startTime: Math.floor(Date.now() / 1000),
timestamp: Math.floor(Date.now() / 1000),
};

console.log("1. Anchoring completed payout record with domain attestation...");
const receipt = await anchorFundablePayout(samplePayout, {
secretKey: payer.secret(),
});

console.log(`- IPFS CID: ${receipt.cid}`);
console.log(`- Attestation Domain: ${receipt.attestation.domain}`);
console.log(`- Attestation Message: ${receipt.attestation.attestationMessage}`);
console.log(`- Signer (Payer): ${receipt.attestation.signerPublicKey}`);
console.log(`- Signature (Base64): ${receipt.attestation.signatureBase64.slice(0, 32)}...\n`);

console.log("2. Performing independent verification (SHA-256 + Ed25519)...");
const verification = verifyAnchoredPayoutReceipt(receipt);

console.log(`- Valid: ${verification.valid ? "YES (Integrity Confirmed)" : "NO"}`);
console.log(`- Hash Recomputed: ${verification.computedHash}`);
console.log(`- Signature Match: ${verification.signatureValid}`);
}

main().catch(console.error);
163 changes: 84 additions & 79 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,84 +1,89 @@
{
"name": "@fundable/sdk",
"version": "0.1.0",
"private": false,
"description": "Multichain TypeScript SDK for Fundable Protocol; Stellar adapter included",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Fundable-Protocol/fundable-sdk.git"
"name": "@fundable/sdk",
"version": "0.1.0",
"private": false,
"description": "Multichain TypeScript SDK for Fundable Protocol; Stellar adapter included",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Fundable-Protocol/fundable-sdk.git"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"docs",
"examples",
"CHANGELOG.md",
"README.md"
],
"homepage": "https://fundable-finance.gitbook.io/fundable-finance-docs/",
"bugs": {
"url": "https://github.com/Fundable-Protocol/fundable-sdk/issues"
},
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
"./core": {
"types": "./dist/core/index.d.ts",
"import": "./dist/core/index.js"
},
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"docs",
"examples",
"CHANGELOG.md",
"README.md"
],
"homepage": "https://fundable-finance.gitbook.io/fundable-finance-docs/",
"bugs": {
"url": "https://github.com/Fundable-Protocol/fundable-sdk/issues"
"./stellar": {
"types": "./dist/stellar/index.d.ts",
"import": "./dist/stellar/index.js"
},
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./core": {
"types": "./dist/core/index.d.ts",
"import": "./dist/core/index.js"
},
"./stellar": {
"types": "./dist/stellar/index.d.ts",
"import": "./dist/stellar/index.js"
}
},
"scripts": {
"clean": "node scripts/clean.mjs",
"build": "pnpm clean && tsc",
"dev": "tsc --watch",
"generate:flow": "node scripts/generate-stellar-binding.mjs flow",
"generate:router": "node scripts/generate-stellar-binding.mjs router",
"generate:stream-nft": "node scripts/generate-stellar-binding.mjs stream_nft",
"generate:paymaster": "node scripts/generate-stellar-binding.mjs paymaster",
"test": "vitest run",
"test:testnet": "FUNDABLE_TESTNET_INTEGRATION=1 vitest run src/stellar/testnet.integration.test.ts",
"typecheck": "tsc --noEmit",
"docs:check": "node scripts/check-doc-links.mjs",
"examples:typecheck": "pnpm build && tsc -p examples/tsconfig.json",
"example:read-flow": "pnpm build && tsx examples/read-flow.ts",
"example:create-router-flow": "pnpm build && tsx examples/create-router-flow.ts",
"example:create-router-lockup": "pnpm build && tsx examples/create-router-lockup.ts",
"example:withdraw-router": "pnpm build && tsx examples/withdraw-router.ts",
"example:read-stream-nft": "pnpm build && tsx examples/read-stream-nft.ts",
"example:check-paymaster": "pnpm build && tsx examples/check-paymaster.ts",
"package:verify": "node scripts/verify-packed-package.mjs",
"verify": "pnpm test && pnpm typecheck && pnpm examples:typecheck && pnpm docs:check",
"prepack": "pnpm verify"
},
"engines": {
"node": ">=20"
},
"packageManager": "pnpm@10.18.0",
"dependencies": {
"@stellar/stellar-sdk": "^14.4.3",
"buffer": "^6.0.3"
},
"devDependencies": {
"@types/node": "^20",
"tsx": "^4.23.1",
"typescript": "^5",
"vitest": "^3.2.4"
},
"peerDependencies": {
"@stellar/stellar-sdk": "^14.4.3"
"./audit": {
"types": "./dist/audit/index.d.ts",
"import": "./dist/audit/index.js"
}
}
},
"scripts": {
"clean": "node scripts/clean.mjs",
"build": "pnpm clean && tsc",
"dev": "tsc --watch",
"generate:flow": "node scripts/generate-stellar-binding.mjs flow",
"generate:router": "node scripts/generate-stellar-binding.mjs router",
"generate:stream-nft": "node scripts/generate-stellar-binding.mjs stream_nft",
"generate:paymaster": "node scripts/generate-stellar-binding.mjs paymaster",
"test": "vitest run",
"test:testnet": "FUNDABLE_TESTNET_INTEGRATION=1 vitest run src/stellar/testnet.integration.test.ts",
"typecheck": "tsc --noEmit",
"docs:check": "node scripts/check-doc-links.mjs",
"examples:typecheck": "pnpm build && tsc -p examples/tsconfig.json",
"example:read-flow": "pnpm build && tsx examples/read-flow.ts",
"example:create-router-flow": "pnpm build && tsx examples/create-router-flow.ts",
"example:create-router-lockup": "pnpm build && tsx examples/create-router-lockup.ts",
"example:withdraw-router": "pnpm build && tsx examples/withdraw-router.ts",
"example:read-stream-nft": "pnpm build && tsx examples/read-stream-nft.ts",
"example:check-paymaster": "pnpm build && tsx examples/check-paymaster.ts",
"example:verify-anchored-payout": "pnpm build && tsx examples/verify-anchored-payout.ts",
"package:verify": "node scripts/verify-packed-package.mjs",
"verify": "pnpm test && pnpm typecheck && pnpm examples:typecheck && pnpm docs:check",
"prepack": "pnpm verify"
},
"engines": {
"node": ">=20"
},
"packageManager": "pnpm@10.18.0",
"dependencies": {
"@stellar/stellar-sdk": "^14.4.3",
"buffer": "^6.0.3"
},
"devDependencies": {
"@types/node": "^20",
"tsx": "^4.23.1",
"typescript": "^5",
"vitest": "^3.2.4"
},
"peerDependencies": {
"@stellar/stellar-sdk": "^14.4.3"
}
}
112 changes: 112 additions & 0 deletions src/audit/adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, it, expect } from "vitest";
import { Keypair } from "@stellar/stellar-sdk";
import {
computeCanonicalAuditHash,
formatAttestationMessage,
signPayoutAttestation,
anchorFundablePayout,
verifyAnchoredPayoutReceipt,
type FundablePayoutAuditRecord,
} from "./index.js";

describe("Nirium Audit Trail Adapter for Fundable Payouts", () => {
const payerKeypair = Keypair.random();
const recipientKeypair = Keypair.random();
const tokenContractId = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";

const sampleRecord: FundablePayoutAuditRecord = {
protocol: "fundable",
chain: "stellar",
networkPassphrase: "Test SDF Network ; September 2015",
contractId: "CA3D5KRYMCMCZVAC7OHQHG2CAUAQ77PDRWNXD7BOD7X62F26T4ZMY54K",
operation: "create_flow",
txHash: "a1b2c3d4e5f67890123456789abcdef0123456789abcdef0123456789abcdef0",
sender: payerKeypair.publicKey(),
recipient: recipientKeypair.publicKey(),
token: tokenContractId,
amountOrRate: "10000000",
startTime: 1724774400,
timestamp: 1724774405,
};

it("computes deterministic canonical SHA-256 hash regardless of key order", () => {
const hash1 = computeCanonicalAuditHash(sampleRecord);

const permutedRecord = {
timestamp: sampleRecord.timestamp,
recipient: sampleRecord.recipient,
token: sampleRecord.token,
amountOrRate: sampleRecord.amountOrRate,
sender: sampleRecord.sender,
operation: sampleRecord.operation,
txHash: sampleRecord.txHash,
contractId: sampleRecord.contractId,
chain: sampleRecord.chain,
networkPassphrase: sampleRecord.networkPassphrase,
protocol: sampleRecord.protocol,
startTime: sampleRecord.startTime,
} as FundablePayoutAuditRecord;

const hash2 = computeCanonicalAuditHash(permutedRecord);
expect(hash1).toBe(hash2);
expect(hash1).toHaveLength(64);
});

it("formats domain-separated attestation message as nirium-audit-v1:<hash>", () => {
const hash = computeCanonicalAuditHash(sampleRecord);
const message = formatAttestationMessage(hash);
expect(message).toBe(`nirium-audit-v1:${hash}`);
});

it("signs and anchors a payout record returning valid receipt and CID", async () => {
const receipt = await anchorFundablePayout(sampleRecord, {
secretKey: payerKeypair.secret(),
});

expect(receipt.cid).toBeDefined();
expect(receipt.cid.startsWith("bafk")).toBe(true);
expect(receipt.attestation.signerPublicKey).toBe(payerKeypair.publicKey());
expect(receipt.attestation.signatureBase64).toBeTruthy();

const verification = verifyAnchoredPayoutReceipt(receipt);
expect(verification.valid).toBe(true);
expect(verification.signatureValid).toBe(true);
expect(verification.signer).toBe(payerKeypair.publicKey());
});

it("rejects verification if receipt payload is tampered", async () => {
const receipt = await anchorFundablePayout(sampleRecord, {
secretKey: payerKeypair.secret(),
});

const tamperedReceipt = {
...receipt,
record: {
...receipt.record,
amountOrRate: "999999999999",
},
};

const verification = verifyAnchoredPayoutReceipt(tamperedReceipt);
expect(verification.valid).toBe(false);
expect(verification.reason).toContain("Hash mismatch");
});

it("rejects verification if signature is invalid", async () => {
const anotherKeypair = Keypair.random();
const receipt = await anchorFundablePayout(sampleRecord, {
secretKey: payerKeypair.secret(),
});

const invalidReceipt = {
...receipt,
attestation: {
...receipt.attestation,
signerPublicKey: anotherKeypair.publicKey(),
},
};

const verification = verifyAnchoredPayoutReceipt(invalidReceipt);
expect(verification.valid).toBe(false);
});
});
Loading