Skip to content

Commit a200095

Browse files
authored
Expose async persistence in FFI (#1287)
2 parents cd617a7 + 6314d3c commit a200095

9 files changed

Lines changed: 618 additions & 31 deletions

File tree

Cargo-minimal.lock

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2658,6 +2658,7 @@ dependencies = [
26582658
name = "payjoin-ffi"
26592659
version = "0.24.0"
26602660
dependencies = [
2661+
"async-trait",
26612662
"bdk",
26622663
"bitcoin-ohttp",
26632664
"getrandom 0.2.15",
@@ -4432,7 +4433,7 @@ dependencies = [
44324433
[[package]]
44334434
name = "uniffi-dart"
44344435
version = "0.1.0"
4435-
source = "git+https://github.com/Uniffi-Dart/uniffi-dart.git?rev=5bdcc79#5bdcc790c3fc99845e7b4a61d7a4f6e1460896e9"
4436+
source = "git+https://github.com/Uniffi-Dart/uniffi-dart.git?rev=f830323#f830323646fb6fbca89f9798dcf425f339f166ca"
44364437
dependencies = [
44374438
"anyhow",
44384439
"camino",
@@ -4502,7 +4503,7 @@ dependencies = [
45024503
[[package]]
45034504
name = "uniffi_dart_macro"
45044505
version = "0.1.0"
4505-
source = "git+https://github.com/Uniffi-Dart/uniffi-dart.git?rev=5bdcc79#5bdcc790c3fc99845e7b4a61d7a4f6e1460896e9"
4506+
source = "git+https://github.com/Uniffi-Dart/uniffi-dart.git?rev=f830323#f830323646fb6fbca89f9798dcf425f339f166ca"
45064507
dependencies = [
45074508
"futures",
45084509
"proc-macro2",

Cargo-recent.lock

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2658,6 +2658,7 @@ dependencies = [
26582658
name = "payjoin-ffi"
26592659
version = "0.24.0"
26602660
dependencies = [
2661+
"async-trait",
26612662
"bdk",
26622663
"bitcoin-ohttp",
26632664
"getrandom 0.2.15",
@@ -4432,7 +4433,7 @@ dependencies = [
44324433
[[package]]
44334434
name = "uniffi-dart"
44344435
version = "0.1.0"
4435-
source = "git+https://github.com/Uniffi-Dart/uniffi-dart.git?rev=5bdcc79#5bdcc790c3fc99845e7b4a61d7a4f6e1460896e9"
4436+
source = "git+https://github.com/Uniffi-Dart/uniffi-dart.git?rev=f830323#f830323646fb6fbca89f9798dcf425f339f166ca"
44364437
dependencies = [
44374438
"anyhow",
44384439
"camino",
@@ -4502,7 +4503,7 @@ dependencies = [
45024503
[[package]]
45034504
name = "uniffi_dart_macro"
45044505
version = "0.1.0"
4505-
source = "git+https://github.com/Uniffi-Dart/uniffi-dart.git?rev=5bdcc79#5bdcc790c3fc99845e7b4a61d7a4f6e1460896e9"
4506+
source = "git+https://github.com/Uniffi-Dart/uniffi-dart.git?rev=f830323#f830323646fb6fbca89f9798dcf425f339f166ca"
45064507
dependencies = [
45074508
"futures",
45084509
"proc-macro2",

payjoin-ffi/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ name = "uniffi-bindgen"
2222
path = "uniffi-bindgen.rs"
2323

2424
[dependencies]
25+
async-trait = "0.1"
2526
getrandom = "0.2"
2627
lazy_static = "1.5.0"
2728
ohttp = { package = "bitcoin-ohttp", version = "0.6.0" }
@@ -32,7 +33,7 @@ serde_json = "1.0.142"
3233
thiserror = "2.0.14"
3334
tokio = { version = "1.47.1", features = ["full"], optional = true }
3435
uniffi = { version = "0.30.0", features = ["cli"] }
35-
uniffi-dart = { git = "https://github.com/Uniffi-Dart/uniffi-dart.git", rev = "5bdcc79", optional = true }
36+
uniffi-dart = { git = "https://github.com/Uniffi-Dart/uniffi-dart.git", rev = "f830323", optional = true }
3637
url = "2.5.4"
3738

3839
# getrandom is ignored here because it's required by the wasm_js feature

payjoin-ffi/dart/test/test_payjoin_unit_test.dart

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,54 @@ class InMemorySenderPersister implements payjoin.JsonSenderSessionPersister {
5050
}
5151
}
5252

53+
class InMemoryReceiverPersisterAsync
54+
implements payjoin.JsonReceiverSessionPersisterAsync {
55+
final String id;
56+
final List<String> events = [];
57+
bool closed = false;
58+
59+
InMemoryReceiverPersisterAsync(this.id);
60+
61+
@override
62+
Future<void> save(String event) async {
63+
events.add(event);
64+
}
65+
66+
@override
67+
Future<List<String>> load() async {
68+
return events;
69+
}
70+
71+
@override
72+
Future<void> close() async {
73+
closed = true;
74+
}
75+
}
76+
77+
class InMemorySenderPersisterAsync
78+
implements payjoin.JsonSenderSessionPersisterAsync {
79+
final String id;
80+
final List<String> events = [];
81+
bool closed = false;
82+
83+
InMemorySenderPersisterAsync(this.id);
84+
85+
@override
86+
Future<void> save(String event) async {
87+
events.add(event);
88+
}
89+
90+
@override
91+
Future<List<String>> load() async {
92+
return events;
93+
}
94+
95+
@override
96+
Future<void> close() async {
97+
closed = true;
98+
}
99+
}
100+
53101
void main() {
54102
group('Test URIs', () {
55103
test('Test todo url encoded', () {
@@ -118,9 +166,9 @@ void main() {
118166
).build().save(persister);
119167
final result = payjoin.replayReceiverEventLog(persister);
120168
expect(
121-
result,
122-
isA<payjoin.ReplayResult>(),
123-
reason: "persistence should return a replay result",
169+
result.state(),
170+
isA<payjoin.InitializedReceiveSession>(),
171+
reason: "receiver should be in Initialized state",
124172
);
125173
});
126174

@@ -142,14 +190,70 @@ void main() {
142190
var sender_persister = InMemorySenderPersister("1");
143191
var psbt =
144192
"cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=";
145-
final result = payjoin.SenderBuilder(
193+
payjoin.SenderBuilder(
146194
psbt,
147195
uri,
148196
).buildRecommended(1000).save(sender_persister);
197+
final senderResult = payjoin.replaySenderEventLog(sender_persister);
149198
expect(
150-
result,
151-
isA<payjoin.WithReplyKey>(),
152-
reason: "persistence should return a reply key",
199+
senderResult.state(),
200+
isA<payjoin.WithReplyKeySendSession>(),
201+
reason: "sender should be in WithReplyKey state",
202+
);
203+
});
204+
});
205+
206+
group("Test Async Persistence", () {
207+
test("Test receiver async persistence", () async {
208+
var persister = InMemoryReceiverPersisterAsync("1");
209+
await payjoin.ReceiverBuilder(
210+
"tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4",
211+
"https://example.com",
212+
payjoin.OhttpKeys.decode(
213+
Uint8List.fromList(
214+
hex.decode(
215+
"01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003",
216+
),
217+
),
218+
),
219+
).build().saveAsync(persister);
220+
final result = await payjoin.replayReceiverEventLogAsync(persister);
221+
expect(
222+
result.state(),
223+
isA<payjoin.InitializedReceiveSession>(),
224+
reason: "receiver should be in Initialized state",
225+
);
226+
});
227+
228+
test("Test sender async persistence", () async {
229+
var receiver_persister = InMemoryReceiverPersisterAsync("1");
230+
var receiver = await payjoin.ReceiverBuilder(
231+
"2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK",
232+
"https://example.com",
233+
payjoin.OhttpKeys.decode(
234+
Uint8List.fromList(
235+
hex.decode(
236+
"01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003",
237+
),
238+
),
239+
),
240+
).build().saveAsync(receiver_persister);
241+
var uri = receiver.pjUri();
242+
243+
var sender_persister = InMemorySenderPersisterAsync("1");
244+
var psbt =
245+
"cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=";
246+
await payjoin.SenderBuilder(
247+
psbt,
248+
uri,
249+
).buildRecommended(1000).saveAsync(sender_persister);
250+
final senderResult = await payjoin.replaySenderEventLogAsync(
251+
sender_persister,
252+
);
253+
expect(
254+
senderResult.state(),
255+
isA<payjoin.WithReplyKeySendSession>(),
256+
reason: "sender should be in WithReplyKey state",
153257
);
154258
});
155259
});

payjoin-ffi/javascript/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

payjoin-ffi/javascript/test/unit.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,54 @@ class InMemorySenderPersister {
5454
}
5555
}
5656

57+
class InMemoryReceiverPersisterAsync {
58+
id: number;
59+
events: any[];
60+
closed: boolean;
61+
62+
constructor(id: number) {
63+
this.id = id;
64+
this.events = [];
65+
this.closed = false;
66+
}
67+
68+
async save(event: any): Promise<void> {
69+
this.events.push(event);
70+
}
71+
72+
async load(): Promise<any[]> {
73+
return this.events;
74+
}
75+
76+
async close(): Promise<void> {
77+
this.closed = true;
78+
}
79+
}
80+
81+
class InMemorySenderPersisterAsync {
82+
id: number;
83+
events: any[];
84+
closed: boolean;
85+
86+
constructor(id: number) {
87+
this.id = id;
88+
this.events = [];
89+
this.closed = false;
90+
}
91+
92+
async save(event: any): Promise<void> {
93+
this.events.push(event);
94+
}
95+
96+
async load(): Promise<any[]> {
97+
return this.events;
98+
}
99+
100+
async close(): Promise<void> {
101+
this.closed = true;
102+
}
103+
}
104+
57105
describe("URI tests", () => {
58106
test("URL encoded payjoin parameter", () => {
59107
const uri =
@@ -172,6 +220,76 @@ describe("Persistence tests", () => {
172220
});
173221
});
174222

223+
describe("Async Persistence tests", () => {
224+
test("receiver async persistence", async () => {
225+
const persister = new InMemoryReceiverPersisterAsync(1);
226+
const address = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4";
227+
const ohttpKeys = payjoin.OhttpKeys.decode(
228+
new Uint8Array([
229+
0x01, 0x00, 0x16, 0x04, 0xba, 0x48, 0xc4, 0x9c, 0x3d, 0x4a,
230+
0x92, 0xa3, 0xad, 0x00, 0xec, 0xc6, 0x3a, 0x02, 0x4d, 0xa1,
231+
0x0c, 0xed, 0x02, 0x18, 0x0c, 0x73, 0xec, 0x12, 0xd8, 0xa7,
232+
0xad, 0x2c, 0xc9, 0x1b, 0xb4, 0x83, 0x82, 0x4f, 0xe2, 0xbe,
233+
0xe8, 0xd2, 0x8b, 0xfe, 0x2e, 0xb2, 0xfc, 0x64, 0x53, 0xbc,
234+
0x4d, 0x31, 0xcd, 0x85, 0x1e, 0x8a, 0x65, 0x40, 0xe8, 0x6c,
235+
0x53, 0x82, 0xaf, 0x58, 0x8d, 0x37, 0x09, 0x57, 0x00, 0x04,
236+
0x00, 0x01, 0x00, 0x03,
237+
]).buffer,
238+
);
239+
240+
const builder = new payjoin.ReceiverBuilder(
241+
address,
242+
"https://example.com",
243+
ohttpKeys,
244+
);
245+
await builder.build().saveAsync(persister);
246+
247+
const result = await payjoin.replayReceiverEventLogAsync(persister);
248+
const state = result.state();
249+
250+
assert.strictEqual(
251+
state.tag,
252+
"Initialized",
253+
"State should be Initialized",
254+
);
255+
});
256+
257+
test("sender async persistence", async () => {
258+
const persister = new InMemoryReceiverPersisterAsync(1);
259+
const address = "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK";
260+
const ohttpKeys = payjoin.OhttpKeys.decode(
261+
new Uint8Array([
262+
0x01, 0x00, 0x16, 0x04, 0xba, 0x48, 0xc4, 0x9c, 0x3d, 0x4a,
263+
0x92, 0xa3, 0xad, 0x00, 0xec, 0xc6, 0x3a, 0x02, 0x4d, 0xa1,
264+
0x0c, 0xed, 0x02, 0x18, 0x0c, 0x73, 0xec, 0x12, 0xd8, 0xa7,
265+
0xad, 0x2c, 0xc9, 0x1b, 0xb4, 0x83, 0x82, 0x4f, 0xe2, 0xbe,
266+
0xe8, 0xd2, 0x8b, 0xfe, 0x2e, 0xb2, 0xfc, 0x64, 0x53, 0xbc,
267+
0x4d, 0x31, 0xcd, 0x85, 0x1e, 0x8a, 0x65, 0x40, 0xe8, 0x6c,
268+
0x53, 0x82, 0xaf, 0x58, 0x8d, 0x37, 0x09, 0x57, 0x00, 0x04,
269+
0x00, 0x01, 0x00, 0x03,
270+
]).buffer,
271+
);
272+
273+
const receiver = await new payjoin.ReceiverBuilder(
274+
address,
275+
"https://example.com",
276+
ohttpKeys,
277+
)
278+
.build()
279+
.saveAsync(persister);
280+
const uri = receiver.pjUri();
281+
282+
const senderPersister = new InMemorySenderPersisterAsync(1);
283+
const psbt =
284+
"cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=";
285+
const withReplyKey = await new payjoin.SenderBuilder(psbt, uri)
286+
.buildRecommended(BigInt(1000))
287+
.saveAsync(senderPersister);
288+
289+
assert.ok(withReplyKey, "Sender should be created successfully");
290+
});
291+
});
292+
175293
describe("Validation", () => {
176294
test("receiver builder rejects bad address", () => {
177295
assert.throws(() => {

0 commit comments

Comments
 (0)