Skip to content
Merged
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
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,16 @@ jobs:
- name: Lint
run: pnpm lint

- name: Test
run: pnpm test
- name: Test with coverage
run: pnpm test:coverage

- name: Upload SDK coverage report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sdk-coverage
path: coverage/
if-no-files-found: error

# Last: the slowest step, and the one the earlier checks usually predict.
- name: Build
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"start": "next start",
"lint": "eslint",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"export:emails": "tsx scripts/export-emails.ts"
},
Expand All @@ -36,6 +37,7 @@
"@types/node": "^26",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitest/coverage-v8": "^3.2.7",
"eslint": "^9",
"eslint-config-next": "16.4.0",
"tailwindcss": "^4",
Expand Down
68 changes: 68 additions & 0 deletions src/lib/sdk/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,70 @@ describe("transaction building + signing", () => {
});
const tx = decodeTx(xdr, Networks.TESTNET);
expect(tx.source).toBe(VECTOR_ACCOUNT);
expect(tx.networkPassphrase).toBe(info.network_passphrase);
expect(tx.signatures.length).toBe(1);
// The signature must verify against the wallet's public key.
const kp = Keypair.fromPublicKey(VECTOR_ACCOUNT);
expect(kp.verify(tx.hash(), tx.signatures[0]!.signature())).toBe(true);
});

it("builds a signed payment to a muxed destination", () => {
const w = fromMnemonic(VECTOR_MNEMONIC);
const destination = new MuxedAccount(
new Account("GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX", "0"),
"42",
).accountId();
const xdr = buildSignedPayment(w.keypair, info, { destination, amount: "1" });
const tx = decodeTx(xdr, info.network_passphrase);
const operation = tx.operations[0]!;

expect(tx.signatures).toHaveLength(1);
expect(tx.networkPassphrase).toBe(info.network_passphrase);
expect(operation.type).toBe("payment");
if (operation.type !== "payment") return;
expect(operation.destination).toBe(destination);
});

it.each([
["invalid destination", "not-a-stellar-address", "1"],
["amount with more than seven decimals", "GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX", "1.00000001"],
["zero amount", "GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX", "0"],
["negative amount", "GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX", "-1"],
])("buildSignedPayment rejects %s", (_reason, destination, amount) => {
const w = fromMnemonic(VECTOR_MNEMONIC);
expect(() => buildSignedPayment(w.keypair, info, { destination, amount })).toThrow();
});

it("buildSignedPayment rejects an invalid asset issuer", () => {
const w = fromMnemonic(VECTOR_MNEMONIC);
expect(() =>
buildSignedPayment(w.keypair, info, {
destination: "GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX",
amount: "1",
asset: { code: "USDC", issuer: "not-a-stellar-address" },
}),
).toThrow();
});

it.each([
["invalid destination", "not-a-stellar-address", "1"],
["amount with more than seven decimals", "GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX", "1.00000001"],
["zero amount", "GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX", "0"],
["negative amount", "GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX", "-1"],
])("buildUnsignedPayment rejects %s", (_reason, destination, amount) => {
expect(() => buildUnsignedPayment(VECTOR_ACCOUNT, info, { destination, amount })).toThrow();
});

it("buildUnsignedPayment rejects an invalid asset issuer", () => {
expect(() =>
buildUnsignedPayment(VECTOR_ACCOUNT, info, {
destination: "GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX",
amount: "1",
asset: { code: "USDC", issuer: "not-a-stellar-address" },
}),
).toThrow();
});

it("builds a signed change-trust (USDC) envelope", () => {
const w = fromMnemonic(VECTOR_MNEMONIC);
const xdr = buildSignedChangeTrust(w.keypair, info, {
Expand All @@ -121,9 +179,19 @@ describe("transaction building + signing", () => {
});
const tx = decodeTx(xdr, Networks.TESTNET);
expect(tx.source).toBe(VECTOR_ACCOUNT);
expect(tx.networkPassphrase).toBe(info.network_passphrase);
expect(tx.signatures.length).toBe(1);
});

it("buildSignedChangeTrust rejects an invalid asset issuer", () => {
const w = fromMnemonic(VECTOR_MNEMONIC);
expect(() =>
buildSignedChangeTrust(w.keypair, info, {
asset: { code: "USDC", issuer: "not-a-stellar-address" },
}),
).toThrow();
});

it("builds a signed create-account for an unfunded XLM destination", () => {
const w = fromMnemonic(VECTOR_MNEMONIC);
const destination = "GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX";
Expand Down
14 changes: 14 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,18 @@ export default defineConfig({
resolve: {
alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },
},
test: {
coverage: {
provider: "v8",
include: ["src/lib/sdk/**/*.ts"],
all: true,
reporter: ["text", "text-summary", "html"],
thresholds: {
branches: 85,
functions: 90,
lines: 90,
statements: 90,
},
},
},
});