this repo has no description
1/**
2 * Integration tests for POST /xrpc/cash.attoshi.submitTransaction
3 */
4
5import { describe, test, expect, beforeAll } from "bun:test";
6import { AttoshiClient } from "../helpers/api.js";
7import { waitForServer, BASE_URL } from "../setup.js";
8
9describe("POST /xrpc/cash.attoshi.submitTransaction", () => {
10 const client = new AttoshiClient();
11
12 beforeAll(async () => {
13 await waitForServer();
14 });
15
16 describe("request validation", () => {
17 test("returns 400 for empty body", async () => {
18 const res = await fetch(
19 `${BASE_URL}/xrpc/cash.attoshi.submitTransaction`,
20 {
21 method: "POST",
22 headers: { "Content-Type": "application/json" },
23 body: "{}",
24 }
25 );
26 expect(res.status).toBe(400);
27 });
28
29 test("returns error for invalid JSON", async () => {
30 const res = await fetch(
31 `${BASE_URL}/xrpc/cash.attoshi.submitTransaction`,
32 {
33 method: "POST",
34 headers: { "Content-Type": "application/json" },
35 body: "invalid json",
36 }
37 );
38 // Server may return 400 or 500 depending on JSON parse error handling
39 expect([400, 500]).toContain(res.status);
40 });
41
42 test("returns NO_INPUTS for transaction without inputs", async () => {
43 const { status, data } = await client.submitTransaction([], [
44 { owner: "did:plc:bob1234567890", amount: 50 },
45 ]);
46 expect(status).toBe(400);
47 expect(data.error).toBe("NO_INPUTS");
48 });
49
50 test("returns NO_OUTPUTS for transaction without outputs", async () => {
51 const { status, data } = await client.submitTransaction(
52 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }],
53 []
54 );
55 expect(status).toBe(400);
56 expect(data.error).toBe("NO_OUTPUTS");
57 });
58 });
59
60 describe("output validation", () => {
61 test("returns INVALID_OUTPUT_OWNER for non-DID owner", async () => {
62 const { status, data } = await client.submitTransaction(
63 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }],
64 [{ owner: "not-a-did", amount: 50 }]
65 );
66 expect(status).toBe(400);
67 expect(data.error).toBe("INVALID_OUTPUT_OWNER");
68 });
69
70 test("returns INVALID_OUTPUT_OWNER for empty owner", async () => {
71 const { status, data } = await client.submitTransaction(
72 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }],
73 [{ owner: "", amount: 50 }]
74 );
75 expect(status).toBe(400);
76 expect(data.error).toBe("INVALID_OUTPUT_OWNER");
77 });
78
79 test("returns INVALID_OUTPUT_AMOUNT for zero amount", async () => {
80 const { status, data } = await client.submitTransaction(
81 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }],
82 [{ owner: "did:plc:bob1234567890", amount: 0 }]
83 );
84 expect(status).toBe(400);
85 expect(data.error).toBe("INVALID_OUTPUT_AMOUNT");
86 });
87
88 test("returns INVALID_OUTPUT_AMOUNT for negative amount", async () => {
89 const { status, data } = await client.submitTransaction(
90 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }],
91 [{ owner: "did:plc:bob1234567890", amount: -100 }]
92 );
93 expect(status).toBe(400);
94 expect(data.error).toBe("INVALID_OUTPUT_AMOUNT");
95 });
96
97 test("returns INVALID_OUTPUT_AMOUNT for non-integer amount", async () => {
98 const { status, data } = await client.submitTransaction(
99 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }],
100 [{ owner: "did:plc:bob1234567890", amount: 50.5 }]
101 );
102 expect(status).toBe(400);
103 expect(data.error).toBe("INVALID_OUTPUT_AMOUNT");
104 });
105 });
106
107 describe("UTXO validation", () => {
108 test("returns UTXO_NOT_FOUND for non-existent input", async () => {
109 const { status, data } = await client.submitTransaction(
110 [{ txId: "nonexistent123", index: 0, sig: "ZmFrZXNpZw==" }],
111 [{ owner: "did:plc:bob1234567890", amount: 50 }]
112 );
113 expect(status).toBe(400);
114 expect(data.error).toBe("UTXO_NOT_FOUND");
115 });
116
117 test("returns UTXO_NOT_FOUND for non-existent output index", async () => {
118 // Get a real UTXO
119 const configRes = await client.getConfig();
120 const treasuryDid = configRes.data.config.treasury;
121 const utxosRes = await client.getUtxos(treasuryDid, 1);
122
123 if (utxosRes.data.utxos.length > 0) {
124 const utxo = utxosRes.data.utxos[0];
125 // Use valid txId but invalid index
126 const { status, data } = await client.submitTransaction(
127 [{ txId: utxo.txId, index: 999, sig: "ZmFrZXNpZw==" }],
128 [{ owner: "did:plc:bob1234567890", amount: 50 }]
129 );
130 expect(status).toBe(400);
131 expect(data.error).toBe("UTXO_NOT_FOUND");
132 }
133 });
134 });
135
136 describe("signature validation", () => {
137 test("returns INVALID_SIGNATURE for bad signature", async () => {
138 // Get a real UTXO
139 const configRes = await client.getConfig();
140 const treasuryDid = configRes.data.config.treasury;
141 const utxosRes = await client.getUtxos(treasuryDid, 1);
142
143 if (utxosRes.data.utxos.length > 0) {
144 const utxo = utxosRes.data.utxos[0];
145 const { status, data } = await client.submitTransaction(
146 [{ txId: utxo.txId, index: utxo.index, sig: "aW52YWxpZHNpZ25hdHVyZQ==" }],
147 [{ owner: "did:plc:bob1234567890", amount: utxo.amount }]
148 );
149 expect(status).toBe(400);
150 expect(data.error).toBe("INVALID_SIGNATURE");
151 }
152 });
153
154 test("returns INVALID_SIGNATURE for malformed base64", async () => {
155 const configRes = await client.getConfig();
156 const treasuryDid = configRes.data.config.treasury;
157 const utxosRes = await client.getUtxos(treasuryDid, 1);
158
159 if (utxosRes.data.utxos.length > 0) {
160 const utxo = utxosRes.data.utxos[0];
161 const { status, data } = await client.submitTransaction(
162 [{ txId: utxo.txId, index: utxo.index, sig: "not-valid-base64!!!" }],
163 [{ owner: "did:plc:bob1234567890", amount: utxo.amount }]
164 );
165 expect(status).toBe(400);
166 }
167 });
168 });
169
170 describe("multiple inputs/outputs", () => {
171 test("returns DUPLICATE_INPUT for same UTXO referenced twice", async () => {
172 const { status, data } = await client.submitTransaction(
173 [
174 { txId: "same-tx-id123", index: 0, sig: "ZmFrZQ==" },
175 { txId: "same-tx-id123", index: 0, sig: "ZmFrZQ==" },
176 ],
177 [{ owner: "did:plc:bob1234567890", amount: 100 }]
178 );
179 expect(status).toBe(400);
180 expect(data.error).toBe("DUPLICATE_INPUT");
181 });
182
183 test("validates all inputs exist", async () => {
184 const { status, data } = await client.submitTransaction(
185 [
186 { txId: "nonexistent1", index: 0, sig: "ZmFrZQ==" },
187 { txId: "nonexistent2", index: 0, sig: "ZmFrZQ==" },
188 ],
189 [{ owner: "did:plc:bob1234567890", amount: 100 }]
190 );
191 expect(status).toBe(400);
192 expect(data.error).toBe("UTXO_NOT_FOUND");
193 });
194
195 test("validates all output owners", async () => {
196 const { status, data } = await client.submitTransaction(
197 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZQ==" }],
198 [
199 { owner: "did:plc:valid1234567", amount: 50 },
200 { owner: "invalid-owner", amount: 50 },
201 ]
202 );
203 expect(status).toBe(400);
204 expect(data.error).toBe("INVALID_OUTPUT_OWNER");
205 });
206
207 test("validates all output amounts", async () => {
208 const { status, data } = await client.submitTransaction(
209 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZQ==" }],
210 [
211 { owner: "did:plc:valid1234567", amount: 50 },
212 { owner: "did:plc:valid2345678", amount: -10 },
213 ]
214 );
215 expect(status).toBe(400);
216 expect(data.error).toBe("INVALID_OUTPUT_AMOUNT");
217 });
218 });
219
220 describe("response structure (on success)", () => {
221 // Note: Testing successful submission requires valid signatures
222 // which would need access to private keys
223 // These tests document the expected response structure
224
225 test("successful response would have txId", async () => {
226 // This is documentation - actual success requires valid signature
227 // Successful response should have:
228 // { txId: string, uri: string, commit: { cid: string, rev: string } }
229 expect(true).toBe(true);
230 });
231
232 test("successful response would have uri", async () => {
233 // URI format: at://{entity-did}/cash.attoshi.tx/{txId}
234 expect(true).toBe(true);
235 });
236
237 test("successful response would have commit info", async () => {
238 // commit: { cid: string, rev: string }
239 expect(true).toBe(true);
240 });
241 });
242});