tally-sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/hook.d.ts +6 -0
- package/dist/src/hook.d.ts.map +1 -0
- package/dist/src/hook.js +37 -0
- package/dist/src/hook.js.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +22 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/middleware/core.d.ts +15 -0
- package/dist/src/middleware/core.d.ts.map +1 -0
- package/dist/src/middleware/core.js +20 -0
- package/dist/src/middleware/core.js.map +1 -0
- package/dist/src/middleware/express.d.ts +4 -0
- package/dist/src/middleware/express.d.ts.map +1 -0
- package/dist/src/middleware/express.js +26 -0
- package/dist/src/middleware/express.js.map +1 -0
- package/dist/src/store.d.ts +14 -0
- package/dist/src/store.d.ts.map +1 -0
- package/dist/src/store.js +84 -0
- package/dist/src/store.js.map +1 -0
- package/dist/src/types.d.ts +36 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +11 -5
- package/src/index.ts +2 -0
- package/src/middleware/core.ts +38 -0
- package/src/middleware/express.ts +32 -0
- package/.opencode/commands/opsx-apply.md +0 -152
- package/.opencode/commands/opsx-archive.md +0 -157
- package/.opencode/commands/opsx-explore.md +0 -169
- package/.opencode/commands/opsx-propose.md +0 -104
- package/.opencode/commands/opsx-sync.md +0 -140
- package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
- package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
- package/.opencode/skills/openspec-explore/SKILL.md +0 -287
- package/.opencode/skills/openspec-propose/SKILL.md +0 -111
- package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
- package/docs/design-hook-store.md +0 -122
- package/docs/plan-hook-store.md +0 -752
- package/test/hook.test.ts +0 -87
- package/test/index.test.ts +0 -36
- package/test/integration.test.ts +0 -44
- package/test/store.test.ts +0 -88
- package/test/types.test.ts +0 -30
package/test/hook.test.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { createTallyFetch } from "../src/hook.js";
|
|
3
|
-
import type { Store, PaymentPayload } from "../src/types.js";
|
|
4
|
-
|
|
5
|
-
function createMockStore(): Store {
|
|
6
|
-
const inserts: PaymentPayload[] = [];
|
|
7
|
-
return {
|
|
8
|
-
insert: vi.fn(async (p: PaymentPayload) => { inserts.push(p); }),
|
|
9
|
-
list: vi.fn(async () => []),
|
|
10
|
-
get: vi.fn(async () => null),
|
|
11
|
-
close: vi.fn(async () => {}),
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe("createTallyFetch", () => {
|
|
16
|
-
let store: Store;
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
store = createMockStore();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("passes through non-payment responses", async () => {
|
|
23
|
-
const mockFetch = vi.fn(async () => new Response("ok", { status: 200 }));
|
|
24
|
-
const tallyFetch = createTallyFetch(mockFetch, store);
|
|
25
|
-
|
|
26
|
-
const res = await tallyFetch("https://api.example.com/data");
|
|
27
|
-
expect(res.status).toBe(200);
|
|
28
|
-
expect(store.insert).not.toHaveBeenCalled();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("extracts PaymentPayload from response with x-facilitator header", async () => {
|
|
32
|
-
const paymentPayload = {
|
|
33
|
-
requestId: "req-1",
|
|
34
|
-
fromAddress: "0xsender",
|
|
35
|
-
toAddress: "0xreceiver",
|
|
36
|
-
amount: "1.50",
|
|
37
|
-
asset: "USDC",
|
|
38
|
-
chainId: "eip155:8453",
|
|
39
|
-
txHash: "0xtx",
|
|
40
|
-
timestamp: Date.now(),
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const mockFetch = vi.fn(async () =>
|
|
44
|
-
Response.json(paymentPayload, {
|
|
45
|
-
status: 200,
|
|
46
|
-
headers: { "x-facilitator": "dexter", "x-request-id": "req-1" },
|
|
47
|
-
})
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
const tallyFetch = createTallyFetch(mockFetch, store, { facilitator: "dexter" });
|
|
51
|
-
await tallyFetch("https://api.example.com/pay");
|
|
52
|
-
|
|
53
|
-
expect(store.insert).toHaveBeenCalledTimes(1);
|
|
54
|
-
const inserted = (store.insert as ReturnType<typeof vi.fn>).mock.calls[0][0] as PaymentPayload;
|
|
55
|
-
expect(inserted.facilitator).toBe("dexter");
|
|
56
|
-
expect(inserted.amount).toBe("1.50");
|
|
57
|
-
expect(inserted.chainId).toBe("eip155:8453");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("generates an id when none is in the response", async () => {
|
|
61
|
-
const mockFetch = vi.fn(async () =>
|
|
62
|
-
Response.json({ amount: "5.00" }, {
|
|
63
|
-
status: 200,
|
|
64
|
-
headers: { "x-facilitator": "dexter", "x-request-id": "req-2" },
|
|
65
|
-
})
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
const tallyFetch = createTallyFetch(mockFetch, store, { facilitator: "dexter" });
|
|
69
|
-
await tallyFetch("https://api.example.com/pay");
|
|
70
|
-
|
|
71
|
-
expect(store.insert).toHaveBeenCalledTimes(1);
|
|
72
|
-
const inserted = (store.insert as ReturnType<typeof vi.fn>).mock.calls[0][0] as PaymentPayload;
|
|
73
|
-
expect(inserted.id).toBeDefined();
|
|
74
|
-
expect(inserted.id.length).toBeGreaterThan(0);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("does not store on error responses", async () => {
|
|
78
|
-
const mockFetch = vi.fn(async () =>
|
|
79
|
-
Response.json({ error: "not found" }, { status: 404 })
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const tallyFetch = createTallyFetch(mockFetch, store);
|
|
83
|
-
const res = await tallyFetch("https://api.example.com/nope");
|
|
84
|
-
expect(res.status).toBe(404);
|
|
85
|
-
expect(store.insert).not.toHaveBeenCalled();
|
|
86
|
-
});
|
|
87
|
-
});
|
package/test/index.test.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { createTally } from "../src/index.js";
|
|
3
|
-
|
|
4
|
-
describe("createTally", () => {
|
|
5
|
-
it("returns a TallyClient with a store", () => {
|
|
6
|
-
const tally = createTally({ facilitator: "dexter" });
|
|
7
|
-
expect(tally.fetch).toBeDefined();
|
|
8
|
-
expect(tally.wrap).toBeDefined();
|
|
9
|
-
expect(tally.unwrap).toBeDefined();
|
|
10
|
-
expect(tally.store).toBeDefined();
|
|
11
|
-
expect(tally.close).toBeDefined();
|
|
12
|
-
tally.close();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("accepts a custom store", () => {
|
|
16
|
-
const customStore = {
|
|
17
|
-
insert: async () => {},
|
|
18
|
-
list: async () => [],
|
|
19
|
-
get: async () => null,
|
|
20
|
-
close: async () => {},
|
|
21
|
-
};
|
|
22
|
-
const tally = createTally({ facilitator: "dexter", store: customStore });
|
|
23
|
-
expect(tally.store).toBe(customStore);
|
|
24
|
-
tally.close();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("wrap and unwrap monkey-patches and restores globalThis.fetch", () => {
|
|
28
|
-
const originalFetch = globalThis.fetch;
|
|
29
|
-
const tally = createTally({ facilitator: "dexter" });
|
|
30
|
-
tally.wrap();
|
|
31
|
-
expect(globalThis.fetch).not.toBe(originalFetch);
|
|
32
|
-
tally.unwrap();
|
|
33
|
-
expect(globalThis.fetch).toBe(originalFetch);
|
|
34
|
-
tally.close();
|
|
35
|
-
});
|
|
36
|
-
});
|
package/test/integration.test.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
-
import { createTally } from "../src/index.js";
|
|
3
|
-
import { SqliteStore } from "../src/store.js";
|
|
4
|
-
import type { Store } from "../src/types.js";
|
|
5
|
-
|
|
6
|
-
describe("integration", () => {
|
|
7
|
-
let store: Store;
|
|
8
|
-
|
|
9
|
-
afterEach(async () => {
|
|
10
|
-
await store.close();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("captures a payment end-to-end", async () => {
|
|
14
|
-
const tally = createTally({ facilitator: "dexter" });
|
|
15
|
-
store = tally.store;
|
|
16
|
-
|
|
17
|
-
// Verify store is ready
|
|
18
|
-
const list = await tally.store.list();
|
|
19
|
-
expect(list).toEqual([]);
|
|
20
|
-
|
|
21
|
-
tally.close();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("manual pipeline: hook captures, store persists", async () => {
|
|
25
|
-
const customStore = new SqliteStore(":memory:");
|
|
26
|
-
store = customStore;
|
|
27
|
-
|
|
28
|
-
const fetchSpy = async () =>
|
|
29
|
-
Response.json(
|
|
30
|
-
{ requestId: "req-int", amount: "3.00", asset: "USDC", chainId: "eip155:1", txHash: "0xint" },
|
|
31
|
-
{ status: 200, headers: { "x-facilitator": "dexter", "x-request-id": "req-int" } }
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const { createTallyFetch } = await import("../src/hook.js");
|
|
35
|
-
const tallyFetch = createTallyFetch(fetchSpy, customStore, { facilitator: "dexter" });
|
|
36
|
-
|
|
37
|
-
await tallyFetch("https://api.example.com/pay");
|
|
38
|
-
|
|
39
|
-
const all = await customStore.list();
|
|
40
|
-
expect(all).toHaveLength(1);
|
|
41
|
-
expect(all[0].amount).toBe("3.00");
|
|
42
|
-
expect(all[0].facilitator).toBe("dexter");
|
|
43
|
-
});
|
|
44
|
-
});
|
package/test/store.test.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { SqliteStore } from "../src/store.js";
|
|
3
|
-
import type { PaymentPayload } from "../src/types.js";
|
|
4
|
-
|
|
5
|
-
function makePayload(overrides?: Partial<PaymentPayload>): PaymentPayload {
|
|
6
|
-
return {
|
|
7
|
-
id: overrides?.id ?? "test-1",
|
|
8
|
-
facilitator: overrides?.facilitator ?? "dexter",
|
|
9
|
-
requestId: "req-1",
|
|
10
|
-
fromAddress: "0xsender",
|
|
11
|
-
toAddress: "0xreceiver",
|
|
12
|
-
amount: "10.00",
|
|
13
|
-
asset: "USDC",
|
|
14
|
-
chainId: "eip155:1",
|
|
15
|
-
txHash: "0xtx",
|
|
16
|
-
timestamp: Date.now(),
|
|
17
|
-
...overrides,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("SqliteStore", () => {
|
|
22
|
-
let store: SqliteStore;
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
store = new SqliteStore(":memory:");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
store.close();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("inserts and retrieves a payment", async () => {
|
|
33
|
-
const payload = makePayload();
|
|
34
|
-
await store.insert(payload);
|
|
35
|
-
const result = await store.get(payload.id);
|
|
36
|
-
expect(result).not.toBeNull();
|
|
37
|
-
expect(result!.id).toBe(payload.id);
|
|
38
|
-
expect(result!.amount).toBe("10.00");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("returns null for unknown id", async () => {
|
|
42
|
-
const result = await store.get("nonexistent");
|
|
43
|
-
expect(result).toBeNull();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("lists payments ordered by created_at desc", async () => {
|
|
47
|
-
const p1 = makePayload({ id: "1", timestamp: 1000 });
|
|
48
|
-
const p2 = makePayload({ id: "2", timestamp: 2000 });
|
|
49
|
-
await store.insert(p1);
|
|
50
|
-
await store.insert(p2);
|
|
51
|
-
const all = await store.list();
|
|
52
|
-
expect(all).toHaveLength(2);
|
|
53
|
-
expect(all[0].id).toBe("2"); // newest first
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("filters by facilitator", async () => {
|
|
57
|
-
await store.insert(makePayload({ id: "1", facilitator: "dexter" }));
|
|
58
|
-
await store.insert(makePayload({ id: "2", facilitator: "coinbase-cdp" }));
|
|
59
|
-
const filtered = await store.list({ facilitator: "dexter" });
|
|
60
|
-
expect(filtered).toHaveLength(1);
|
|
61
|
-
expect(filtered[0].facilitator).toBe("dexter");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("paginates with limit and offset", async () => {
|
|
65
|
-
for (let i = 0; i < 10; i++) {
|
|
66
|
-
await store.insert(makePayload({ id: `p${i}`, timestamp: i }));
|
|
67
|
-
}
|
|
68
|
-
const page = await store.list({ limit: 3, offset: 0 });
|
|
69
|
-
expect(page).toHaveLength(3);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("handles metadata serialization", async () => {
|
|
73
|
-
const payload = makePayload({ metadata: { foo: "bar", num: 42 } });
|
|
74
|
-
await store.insert(payload);
|
|
75
|
-
const result = await store.get(payload.id);
|
|
76
|
-
expect(result!.metadata).toEqual({ foo: "bar", num: 42 });
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("ignores duplicate inserts (idempotency)", async () => {
|
|
80
|
-
const payload = makePayload({ id: "dup-1" });
|
|
81
|
-
await store.insert(payload);
|
|
82
|
-
await store.insert(payload);
|
|
83
|
-
const result = await store.get("dup-1");
|
|
84
|
-
expect(result).not.toBeNull();
|
|
85
|
-
const all = await store.list();
|
|
86
|
-
expect(all).toHaveLength(1);
|
|
87
|
-
});
|
|
88
|
-
});
|
package/test/types.test.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import type { PaymentPayload, Store, TallyConfig, TallyClient } from "../src/types.js";
|
|
3
|
-
|
|
4
|
-
describe("types", () => {
|
|
5
|
-
it("PaymentPayload can be constructed", () => {
|
|
6
|
-
const payload: PaymentPayload = {
|
|
7
|
-
id: "abc-123",
|
|
8
|
-
facilitator: "dexter",
|
|
9
|
-
requestId: "req-1",
|
|
10
|
-
fromAddress: "0xsender",
|
|
11
|
-
toAddress: "0xreceiver",
|
|
12
|
-
amount: "10.00",
|
|
13
|
-
asset: "USDC",
|
|
14
|
-
chainId: "eip155:1",
|
|
15
|
-
txHash: "0xtx",
|
|
16
|
-
timestamp: Date.now(),
|
|
17
|
-
};
|
|
18
|
-
expect(payload.amount).toBe("10.00");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("Store interface is structurally typed", () => {
|
|
22
|
-
const store: Store = {
|
|
23
|
-
insert: async () => {},
|
|
24
|
-
list: async () => [],
|
|
25
|
-
get: async () => null,
|
|
26
|
-
close: async () => {},
|
|
27
|
-
};
|
|
28
|
-
expect(store).toBeDefined();
|
|
29
|
-
});
|
|
30
|
-
});
|