From 2770a861eddce312918626314a05c416c429f6e1 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Sat, 18 Feb 2023 01:07:19 +0800 Subject: [PATCH 01/23] initial escrow upd --- packages/espressocash_app/pubspec.lock | 2 +- .../share_escrow/.gitignore | 7 + .../share_escrow/CHANGELOG.md | 3 + .../share_escrow/README.md | 39 ++++ .../share_escrow/analysis_options.yaml | 1 + .../share_escrow/lib/share_escrow.dart | 3 + .../share_escrow/lib/src/instructions.dart | 99 +++++++++ .../share_escrow/lib/src/models.dart | 85 ++++++++ .../share_escrow/lib/src/models.g.dart | 111 ++++++++++ .../share_escrow/pubspec.yaml | 18 ++ .../share_escrow/tests/escrow_test.dart | 201 ++++++++++++++++++ .../share_escrow/tests/utils/accounts.dart | 20 ++ .../share_escrow/tests/utils/config.dart | 9 + .../share_escrow/tests/utils/helper.dart | 51 +++++ 14 files changed, 648 insertions(+), 1 deletion(-) create mode 100644 packages/espressocash_backend/share_escrow/.gitignore create mode 100644 packages/espressocash_backend/share_escrow/CHANGELOG.md create mode 100644 packages/espressocash_backend/share_escrow/README.md create mode 100644 packages/espressocash_backend/share_escrow/analysis_options.yaml create mode 100644 packages/espressocash_backend/share_escrow/lib/share_escrow.dart create mode 100644 packages/espressocash_backend/share_escrow/lib/src/instructions.dart create mode 100644 packages/espressocash_backend/share_escrow/lib/src/models.dart create mode 100644 packages/espressocash_backend/share_escrow/lib/src/models.g.dart create mode 100644 packages/espressocash_backend/share_escrow/pubspec.yaml create mode 100644 packages/espressocash_backend/share_escrow/tests/escrow_test.dart create mode 100644 packages/espressocash_backend/share_escrow/tests/utils/accounts.dart create mode 100644 packages/espressocash_backend/share_escrow/tests/utils/config.dart create mode 100644 packages/espressocash_backend/share_escrow/tests/utils/helper.dart diff --git a/packages/espressocash_app/pubspec.lock b/packages/espressocash_app/pubspec.lock index 6134b1ad36..249fb01a96 100644 --- a/packages/espressocash_app/pubspec.lock +++ b/packages/espressocash_app/pubspec.lock @@ -1541,7 +1541,7 @@ packages: path: "../solana" relative: true source: path - version: "0.28.1" + version: "0.29.0" solana_seed_vault: dependency: "direct main" description: diff --git a/packages/espressocash_backend/share_escrow/.gitignore b/packages/espressocash_backend/share_escrow/.gitignore new file mode 100644 index 0000000000..3cceda5578 --- /dev/null +++ b/packages/espressocash_backend/share_escrow/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/espressocash_backend/share_escrow/CHANGELOG.md b/packages/espressocash_backend/share_escrow/CHANGELOG.md new file mode 100644 index 0000000000..effe43c82c --- /dev/null +++ b/packages/espressocash_backend/share_escrow/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/espressocash_backend/share_escrow/README.md b/packages/espressocash_backend/share_escrow/README.md new file mode 100644 index 0000000000..8b55e735b5 --- /dev/null +++ b/packages/espressocash_backend/share_escrow/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/espressocash_backend/share_escrow/analysis_options.yaml b/packages/espressocash_backend/share_escrow/analysis_options.yaml new file mode 100644 index 0000000000..511c0ab2fe --- /dev/null +++ b/packages/espressocash_backend/share_escrow/analysis_options.yaml @@ -0,0 +1 @@ +include: package:mews_pedantic/analysis_options.yaml \ No newline at end of file diff --git a/packages/espressocash_backend/share_escrow/lib/share_escrow.dart b/packages/espressocash_backend/share_escrow/lib/share_escrow.dart new file mode 100644 index 0000000000..3aaefa2428 --- /dev/null +++ b/packages/espressocash_backend/share_escrow/lib/share_escrow.dart @@ -0,0 +1,3 @@ +library share_escrow; + +//TODO add exports diff --git a/packages/espressocash_backend/share_escrow/lib/src/instructions.dart b/packages/espressocash_backend/share_escrow/lib/src/instructions.dart new file mode 100644 index 0000000000..0608c04ba2 --- /dev/null +++ b/packages/espressocash_backend/share_escrow/lib/src/instructions.dart @@ -0,0 +1,99 @@ +import 'package:share_escrow/src/models.dart'; +import 'package:solana/anchor.dart'; +import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; + +class EscrowInstruction { + static Future initEscrow({ + required int amount, + required Ed25519HDPublicKey escrowAccount, + required Ed25519HDPublicKey senderAccount, + required Ed25519HDPublicKey senderTokenAccount, + required Ed25519HDPublicKey depositorAccount, + required Ed25519HDPublicKey vaultTokenAccount, + }) => + AnchorInstruction.forMethod( + programId: escrowProgram, + method: 'initialize_escrow', + arguments: ByteArray( + EscrowArgument( + amount: BigInt.from(amount), + ).toBorsh().toList(), + ), + accounts: [ + AccountMeta.writeable(pubKey: escrowAccount, isSigner: true), + AccountMeta.writeable(pubKey: depositorAccount, isSigner: true), + AccountMeta.writeable(pubKey: senderAccount, isSigner: true), + AccountMeta.writeable(pubKey: senderTokenAccount, isSigner: false), + AccountMeta.writeable(pubKey: vaultTokenAccount, isSigner: false), + AccountMeta.readonly(pubKey: SystemProgram.id, isSigner: false), + AccountMeta.readonly(pubKey: TokenProgram.id, isSigner: false), + ], + namespace: 'global', + ); + + static Future completeEscrow({ + required Ed25519HDPublicKey escrowAccount, + required Ed25519HDPublicKey senderTokenAccount, + required Ed25519HDPublicKey receiverTokenAccount, + required Ed25519HDPublicKey depositorAccount, + required Ed25519HDPublicKey vaultTokenAccount, + }) async => + AnchorInstruction.forMethod( + programId: escrowProgram, + method: 'complete_escrow', + accounts: [ + AccountMeta.writeable(pubKey: escrowAccount, isSigner: true), + AccountMeta.writeable(pubKey: depositorAccount, isSigner: true), + AccountMeta.writeable(pubKey: senderTokenAccount, isSigner: false), + AccountMeta.writeable(pubKey: receiverTokenAccount, isSigner: false), + AccountMeta.writeable(pubKey: vaultTokenAccount, isSigner: false), + AccountMeta.readonly( + pubKey: await _calculatePda(escrowAccount), + isSigner: false, + ), + AccountMeta.readonly(pubKey: TokenProgram.id, isSigner: false), + ], + namespace: 'global', + ); + + static Future cancelEscrow({ + required Ed25519HDPublicKey escrowAccount, + required Ed25519HDPublicKey senderAccount, + required Ed25519HDPublicKey senderTokenAccount, + required Ed25519HDPublicKey depositorAccount, + required Ed25519HDPublicKey vaultTokenAccount, + }) async => + AnchorInstruction.forMethod( + programId: escrowProgram, + method: 'cancel_escrow', + accounts: [ + AccountMeta.writeable(pubKey: escrowAccount, isSigner: false), + AccountMeta.writeable(pubKey: senderAccount, isSigner: true), + AccountMeta.writeable(pubKey: depositorAccount, isSigner: true), + AccountMeta.writeable(pubKey: senderTokenAccount, isSigner: false), + AccountMeta.writeable(pubKey: vaultTokenAccount, isSigner: false), + AccountMeta.readonly( + pubKey: await _calculatePda(escrowAccount), + isSigner: false, + ), + AccountMeta.readonly(pubKey: TokenProgram.id, isSigner: false), + ], + namespace: 'global', + ); +} + +Future _calculatePda( + Ed25519HDPublicKey escrowAccount, +) async => + Ed25519HDPublicKey.findProgramAddress( + seeds: [ + 'ec_shareable_links'.codeUnits, + escrowAccount.bytes, + ], + programId: escrowProgram, + ); + +final escrowProgram = Ed25519HDPublicKey.fromBase58( + 'GHrMLBLnwGB8ypCWQnPeRzgHwePpUtSnY5ZSCCWzYmhC', +); diff --git a/packages/espressocash_backend/share_escrow/lib/src/models.dart b/packages/espressocash_backend/share_escrow/lib/src/models.dart new file mode 100644 index 0000000000..d730e495f2 --- /dev/null +++ b/packages/espressocash_backend/share_escrow/lib/src/models.dart @@ -0,0 +1,85 @@ +import 'package:borsh_annotation/borsh_annotation.dart'; +import 'package:solana/anchor.dart'; +import 'package:solana/dto.dart'; +import 'package:solana/solana.dart'; +import 'package:solana/src/borsh_ext.dart'; + +part 'models.g.dart'; + +class EscrowDataAccount implements AnchorAccount { + const EscrowDataAccount._({ + required this.discriminator, + required this.vaultTokenAccount, + required this.status, + required this.senderAccount, + required this.senderTokenAccount, + required this.depositorAccount, + }); + + factory EscrowDataAccount._fromBinary( + List bytes, + ) { + final accountData = + _EscrowDataAccount.fromBorsh(Uint8List.fromList(bytes.sublist(8))); + + return EscrowDataAccount._( + discriminator: bytes.sublist(0, 8), + senderAccount: accountData.senderAccount, + senderTokenAccount: accountData.senderTokenAccount, + depositorAccount: accountData.depositorAccount, + vaultTokenAccount: accountData.vaultTokenAccount, + status: EscrowStatus.values[accountData.status], + ); + } + + factory EscrowDataAccount.fromAccountData(AccountData accountData) { + if (accountData is BinaryAccountData) { + return EscrowDataAccount._fromBinary(accountData.data); + } else { + throw const FormatException('invalid account data found'); + } + } + + @override + final List discriminator; + + final Ed25519HDPublicKey senderAccount; + final Ed25519HDPublicKey vaultTokenAccount; + final Ed25519HDPublicKey senderTokenAccount; + final Ed25519HDPublicKey depositorAccount; + final EscrowStatus status; +} + +@BorshSerializable() +class _EscrowDataAccount with _$_EscrowDataAccount { + factory _EscrowDataAccount({ + @BPublicKey() required Ed25519HDPublicKey senderAccount, + @BPublicKey() required Ed25519HDPublicKey vaultTokenAccount, + @BPublicKey() required Ed25519HDPublicKey senderTokenAccount, + @BPublicKey() required Ed25519HDPublicKey depositorAccount, + @BU8() required int status, + }) = __EscrowDataAccount; + + _EscrowDataAccount._(); + + factory _EscrowDataAccount.fromBorsh(Uint8List data) => + _$_EscrowDataAccountFromBorsh(data); +} + +enum EscrowStatus { + pending, + canceled, + completed; +} + +@BorshSerializable() +class EscrowArgument with _$EscrowArgument { + factory EscrowArgument({ + @BU64() required BigInt amount, + }) = _EscrowArgument; + + EscrowArgument._(); + + factory EscrowArgument.fromBorsh(Uint8List data) => + _$EscrowArgumentFromBorsh(data); +} diff --git a/packages/espressocash_backend/share_escrow/lib/src/models.g.dart b/packages/espressocash_backend/share_escrow/lib/src/models.g.dart new file mode 100644 index 0000000000..8b0fd36823 --- /dev/null +++ b/packages/espressocash_backend/share_escrow/lib/src/models.g.dart @@ -0,0 +1,111 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'models.dart'; + +// ************************************************************************** +// BorshSerializableGenerator +// ************************************************************************** + +mixin _$_EscrowDataAccount { + Ed25519HDPublicKey get senderAccount => throw UnimplementedError(); + Ed25519HDPublicKey get vaultTokenAccount => throw UnimplementedError(); + Ed25519HDPublicKey get senderTokenAccount => throw UnimplementedError(); + Ed25519HDPublicKey get depositorAccount => throw UnimplementedError(); + int get status => throw UnimplementedError(); + + Uint8List toBorsh() { + final writer = BinaryWriter(); + + const BPublicKey().write(writer, senderAccount); + const BPublicKey().write(writer, vaultTokenAccount); + const BPublicKey().write(writer, senderTokenAccount); + const BPublicKey().write(writer, depositorAccount); + const BU8().write(writer, status); + + return writer.toArray(); + } +} + +class __EscrowDataAccount extends _EscrowDataAccount { + __EscrowDataAccount({ + required this.senderAccount, + required this.vaultTokenAccount, + required this.senderTokenAccount, + required this.depositorAccount, + required this.status, + }) : super._(); + + final Ed25519HDPublicKey senderAccount; + final Ed25519HDPublicKey vaultTokenAccount; + final Ed25519HDPublicKey senderTokenAccount; + final Ed25519HDPublicKey depositorAccount; + final int status; +} + +class B_EscrowDataAccount implements BType<_EscrowDataAccount> { + const B_EscrowDataAccount(); + + @override + void write(BinaryWriter writer, _EscrowDataAccount value) { + writer.writeStruct(value.toBorsh()); + } + + @override + _EscrowDataAccount read(BinaryReader reader) { + return _EscrowDataAccount( + senderAccount: const BPublicKey().read(reader), + vaultTokenAccount: const BPublicKey().read(reader), + senderTokenAccount: const BPublicKey().read(reader), + depositorAccount: const BPublicKey().read(reader), + status: const BU8().read(reader), + ); + } +} + +_EscrowDataAccount _$_EscrowDataAccountFromBorsh(Uint8List data) { + final reader = BinaryReader(data.buffer.asByteData()); + + return const B_EscrowDataAccount().read(reader); +} + +mixin _$EscrowArgument { + BigInt get amount => throw UnimplementedError(); + + Uint8List toBorsh() { + final writer = BinaryWriter(); + + const BU64().write(writer, amount); + + return writer.toArray(); + } +} + +class _EscrowArgument extends EscrowArgument { + _EscrowArgument({ + required this.amount, + }) : super._(); + + final BigInt amount; +} + +class BEscrowArgument implements BType { + const BEscrowArgument(); + + @override + void write(BinaryWriter writer, EscrowArgument value) { + writer.writeStruct(value.toBorsh()); + } + + @override + EscrowArgument read(BinaryReader reader) { + return EscrowArgument( + amount: const BU64().read(reader), + ); + } +} + +EscrowArgument _$EscrowArgumentFromBorsh(Uint8List data) { + final reader = BinaryReader(data.buffer.asByteData()); + + return const BEscrowArgument().read(reader); +} diff --git a/packages/espressocash_backend/share_escrow/pubspec.yaml b/packages/espressocash_backend/share_escrow/pubspec.yaml new file mode 100644 index 0000000000..81cd31f3ec --- /dev/null +++ b/packages/espressocash_backend/share_escrow/pubspec.yaml @@ -0,0 +1,18 @@ +name: share_escrow +description: A starting point for Dart libraries or applications. +version: 1.0.0 +publish_to: "none" + +environment: + sdk: '>=2.19.2 <3.0.0' + +dependencies: + borsh: ^0.3.1+2 + borsh_annotation: ^0.3.1+3 + solana: ^0.29.0 + +dev_dependencies: + build_runner: ^2.3.2 + json_serializable: ^6.5.2 + mews_pedantic: ^0.13.0 + test: ^1.22.1 \ No newline at end of file diff --git a/packages/espressocash_backend/share_escrow/tests/escrow_test.dart b/packages/espressocash_backend/share_escrow/tests/escrow_test.dart new file mode 100644 index 0000000000..d0fa9c06fb --- /dev/null +++ b/packages/espressocash_backend/share_escrow/tests/escrow_test.dart @@ -0,0 +1,201 @@ +import 'package:share_escrow/src/instructions.dart'; +import 'package:share_escrow/src/models.dart'; +import 'package:solana/dto.dart'; +import 'package:solana/solana.dart'; +import 'package:test/test.dart'; + +import 'utils/accounts.dart'; +import 'utils/config.dart'; +import 'utils/helper.dart'; + +void main() { + final client = createTestSolanaClient(); + + late Mint mint; + late Ed25519HDKeyPair mintAuthority; + + const initializerAmount = 1000; + const sendAmount = 700; + + const escrowAccountRent = 1893120; // calculated for escrow account len = 144 + const vaultTokenRent = 2039280; // calculated for token account len = 165 + const airdropAmount = 1000000000; + + const expectedDepositorBalanceAfterInit = airdropAmount - escrowAccountRent; + const expectedDepositorBalanceAfterComplete = + airdropAmount - escrowAccountRent + vaultTokenRent; + + Future requestAirdrop(Ed25519HDKeyPair account) async { + await client.requestAirdrop( + address: account.publicKey, + lamports: airdropAmount, + commitment: Commitment.confirmed, + ); + } + + setUpAll(() async { + mintAuthority = await Ed25519HDKeyPair.random(); + + await requestAirdrop(mintAuthority); + + mint = await client.initializeMint( + mintAuthority: mintAuthority, + decimals: 5, + commitment: Commitment.confirmed, + ); + }); + + Future fetchEscrowAccount( + Ed25519HDPublicKey escrowAccount, + ) async { + final account = await client.rpcClient.getAccountInfo( + escrowAccount.toBase58(), + encoding: Encoding.base64, + commitment: Commitment.confirmed, + ); + + return EscrowDataAccount.fromAccountData(account.value!.data!); + } + + Future createEscrow(Accounts accounts) async { + final instruction = await EscrowInstruction.initEscrow( + amount: sendAmount, + escrowAccount: accounts.escrowAccount.publicKey, + senderAccount: accounts.senderAccount.publicKey, + depositorAccount: accounts.depositorAccount.publicKey, + senderTokenAccount: accounts.senderTokenAccount, + vaultTokenAccount: accounts.vaultTokenAccount, + ); + + await client.sendAndConfirmTransaction( + message: Message.only(instruction), + signers: [ + accounts.depositorAccount, + accounts.escrowAccount, + accounts.senderAccount, + ], + commitment: Commitment.confirmed, + ); + } + + Future createAccounts() async { + final Ed25519HDKeyPair escrow = await Ed25519HDKeyPair.random(); + final Ed25519HDKeyPair sender = await Ed25519HDKeyPair.random(); + final Ed25519HDKeyPair receiver = await Ed25519HDKeyPair.random(); + final Ed25519HDKeyPair depositor = await Ed25519HDKeyPair.random(); + + await requestAirdrop(sender); + await requestAirdrop(receiver); + await requestAirdrop(depositor); + + final sellerTokenAccount = + await createAccount(client: client, owner: sender, mint: mint); + + await client.mintTo( + mint: mint.address, + destination: sellerTokenAccount, + amount: initializerAmount, + authority: mintAuthority, + commitment: Commitment.confirmed, + ); + + return Accounts( + escrowAccount: escrow, + senderAccount: sender, + receiverAccount: receiver, + depositorAccount: depositor, + senderTokenAccount: sellerTokenAccount, + receiverTokenAccount: + await createAccount(client: client, owner: receiver, mint: mint), + vaultTokenAccount: + await createAccount(client: client, owner: sender, mint: mint), + ); + } + + test('Initialize Escrow', () async { + final accounts = await createAccounts(); + + await createEscrow(accounts); + + final escrowAccount = + await fetchEscrowAccount(accounts.escrowAccount.publicKey); + + expect(EscrowStatus.pending, escrowAccount.status); + + final senderTokenAccount = await getTokenAccountBalance( + client: client, + account: accounts.senderTokenAccount, + ); + + expect( + initializerAmount - sendAmount, + int.tryParse(senderTokenAccount!.amount) ?? 0, + ); + + final depositorTokenAccount = await getTokenAccountBalance( + client: client, + account: accounts.vaultTokenAccount, + ); + + expect( + sendAmount, + int.tryParse(depositorTokenAccount!.amount) ?? 0, + ); + }); + + test('Cancel Escrow', () async { + final accounts = await createAccounts(); + + await createEscrow(accounts); + + final instruction = await EscrowInstruction.cancelEscrow( + escrowAccount: accounts.escrowAccount.publicKey, + senderAccount: accounts.senderAccount.publicKey, + depositorAccount: accounts.depositorAccount.publicKey, + senderTokenAccount: accounts.senderTokenAccount, + vaultTokenAccount: accounts.vaultTokenAccount, + ); + + await client.sendAndConfirmTransaction( + message: Message.only(instruction), + signers: [ + accounts.depositorAccount, + accounts.senderAccount, + ], + commitment: Commitment.confirmed, + ); + + final escrowAccount = + await fetchEscrowAccount(accounts.escrowAccount.publicKey); + + expect(EscrowStatus.canceled, escrowAccount.status); + }); + + test('Complete Escrow', () async { + final accounts = await createAccounts(); + + await createEscrow(accounts); + + final instruction = await EscrowInstruction.completeEscrow( + escrowAccount: accounts.escrowAccount.publicKey, + receiverTokenAccount: accounts.receiverTokenAccount, + depositorAccount: accounts.depositorAccount.publicKey, + senderTokenAccount: accounts.senderTokenAccount, + vaultTokenAccount: accounts.vaultTokenAccount, + ); + + await client.sendAndConfirmTransaction( + message: Message.only(instruction), + signers: [ + accounts.depositorAccount, + accounts.escrowAccount, + ], + commitment: Commitment.confirmed, + ); + + final escrowAccount = + await fetchEscrowAccount(accounts.escrowAccount.publicKey); + + expect(EscrowStatus.completed, escrowAccount.status); + }); +} diff --git a/packages/espressocash_backend/share_escrow/tests/utils/accounts.dart b/packages/espressocash_backend/share_escrow/tests/utils/accounts.dart new file mode 100644 index 0000000000..a958675ee9 --- /dev/null +++ b/packages/espressocash_backend/share_escrow/tests/utils/accounts.dart @@ -0,0 +1,20 @@ +import 'package:solana/solana.dart'; + +class Accounts { + Accounts({ + required this.escrowAccount, + required this.receiverAccount, + required this.senderTokenAccount, + required this.vaultTokenAccount, + required this.senderAccount, + required this.depositorAccount, + required this.receiverTokenAccount, + }); + final Ed25519HDKeyPair escrowAccount; + final Ed25519HDKeyPair receiverAccount; + final Ed25519HDKeyPair senderAccount; + final Ed25519HDKeyPair depositorAccount; + final Ed25519HDPublicKey senderTokenAccount; + final Ed25519HDPublicKey receiverTokenAccount; + final Ed25519HDPublicKey vaultTokenAccount; +} diff --git a/packages/espressocash_backend/share_escrow/tests/utils/config.dart b/packages/espressocash_backend/share_escrow/tests/utils/config.dart new file mode 100644 index 0000000000..d410468dd1 --- /dev/null +++ b/packages/espressocash_backend/share_escrow/tests/utils/config.dart @@ -0,0 +1,9 @@ +import 'package:solana/solana.dart'; + +const devnetRpcUrl = 'http://127.0.0.1:8899'; +const devnetWebsocketUrl = 'ws://127.0.0.1:8900'; + +SolanaClient createTestSolanaClient() => SolanaClient( + rpcUrl: Uri.parse(devnetRpcUrl), + websocketUrl: Uri.parse(devnetWebsocketUrl), + ); diff --git a/packages/espressocash_backend/share_escrow/tests/utils/helper.dart b/packages/espressocash_backend/share_escrow/tests/utils/helper.dart new file mode 100644 index 0000000000..d436431156 --- /dev/null +++ b/packages/espressocash_backend/share_escrow/tests/utils/helper.dart @@ -0,0 +1,51 @@ +import 'package:solana/dto.dart'; +import 'package:solana/solana.dart'; + +Future createAccount({ + required SolanaClient client, + required Ed25519HDKeyPair owner, + required Mint mint, +}) async { + final accountKeyPair = await Ed25519HDKeyPair.random(); + + await client.createTokenAccount( + mint: mint.address, + account: accountKeyPair, + creator: owner, + commitment: Commitment.confirmed, + ); + + return accountKeyPair.publicKey; +} + +Future getTokenAccountBalance({ + required SolanaClient client, + required Ed25519HDPublicKey account, +}) async { + final info = await client.rpcClient.getAccountInfo( + account.toBase58(), + encoding: Encoding.jsonParsed, + commitment: Commitment.confirmed, + ); + + final accountData = info.value?.data; + + if (accountData is ParsedAccountData) { + return accountData.maybeWhen( + splToken: (parsed) => parsed.maybeMap( + account: (a) => a.info.tokenAmount, + orElse: () => null, + ), + orElse: () => null, + ); + } + + return null; +} + +Future getTokenTest({ + required SolanaClient client, + required Ed25519HDPublicKey account, + required Ed25519HDPublicKey mint, +}) async => + client.getTokenBalance(owner: account, mint: mint); From 3532a29ce505be0c81aab121869975a5317099ad Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Tue, 21 Feb 2023 13:11:17 +0800 Subject: [PATCH 02/23] upd --- .../lib/core/transactions/resign_tx.dart | 6 +- .../src/bl/oskp_service.dart | 15 +- .../widgets/extensions.dart | 2 +- .../espressocash_api/lib/src/client.dart | 5 + .../espressocash_api/lib/src/client.g.dart | 24 ++ .../lib/src/dto/create_payment.dart | 23 ++ .../lib/src/dto/create_payment.freezed.dart | 343 ++++++++++++++++++ .../lib/src/dto/create_payment.g.dart | 30 ++ .../lib/src/payments/cancel_payment.dart | 66 ++++ .../lib/src/payments/create_payment.dart | 22 +- .../lib/src/payments/handler.dart | 25 ++ .../lib/src/payments/receive_payment.dart | 21 +- packages/espressocash_backend/pubspec.lock | 17 +- packages/espressocash_backend/pubspec.yaml | 2 + .../share_escrow/lib/share_escrow.dart | 3 +- .../share_escrow/lib/src/instructions.dart | 2 - .../share_escrow/tests/escrow_test.dart | 17 +- .../payments/create_direct_payment_test.dart | 4 +- .../test/payments/create_payment_test.dart | 4 +- .../test/payments/escrow_account_test.dart | 2 +- .../test/payments/receive_payment_test.dart | 8 +- .../test/payments/utils.dart | 23 +- 22 files changed, 599 insertions(+), 65 deletions(-) create mode 100644 packages/espressocash_backend/lib/src/payments/cancel_payment.dart diff --git a/packages/espressocash_app/lib/core/transactions/resign_tx.dart b/packages/espressocash_app/lib/core/transactions/resign_tx.dart index 1cc40996ca..2f5603939c 100644 --- a/packages/espressocash_app/lib/core/transactions/resign_tx.dart +++ b/packages/espressocash_app/lib/core/transactions/resign_tx.dart @@ -5,8 +5,10 @@ import '../accounts/bl/ec_wallet.dart'; extension ResignTx on SignedTx { Future resign(ECWallet wallet) async => SignedTx( signatures: signatures.toList() - ..removeLast() - ..add(await wallet.sign(compiledMessage.toByteArray())), + ..setAll( + signatures.indexWhere((it) => it.publicKey == wallet.publicKey), [ + await wallet.sign(compiledMessage.toByteArray()), + ]), compiledMessage: compiledMessage, ); } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart index 4b5c7d9aeb..954b4b5c0c 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart @@ -55,7 +55,7 @@ class OSKPService { Future cancel( OutgoingSplitKeyPayment payment, { - required Ed25519HDPublicKey account, + required ECWallet account, }) async { final status = payment.status; @@ -105,7 +105,8 @@ class OSKPService { final response = await _client.createPayment(dto); final tx = await response.transaction .let(SignedTx.decode) - .let((it) => it.resign(account)); + .let((it) => it.resign(account)) + .letAsync((it) => it.resign(LocalWallet(escrowAccount))); return OSKPStatus.txCreated(tx, escrow: privateKey, slot: response.slot); } on Exception { @@ -117,22 +118,22 @@ class OSKPService { Future _createCancelTx({ required Ed25519HDKeyPair escrow, - required Ed25519HDPublicKey account, + required ECWallet account, }) async { final privateKey = await EscrowPrivateKey.fromKeyPair(escrow); try { - final dto = ReceivePaymentRequestDto( - receiverAccount: account.toBase58(), + final dto = CancelPaymentRequestDto( + senderAccount: account.address, escrowAccount: escrow.address, cluster: apiCluster, ); - final response = await _client.receivePayment(dto); + final response = await _client.cancelPayment(dto); final tx = await response .let((it) => it.transaction) .let(SignedTx.decode) - .let((it) => it.resign(LocalWallet(escrow))); + .let((it) => it.resign(account)); return OSKPStatus.cancelTxCreated( tx, diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/widgets/extensions.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/widgets/extensions.dart index 92472f0d00..3685e276d9 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/widgets/extensions.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/widgets/extensions.dart @@ -28,7 +28,7 @@ extension BuildContextExt on BuildContext { runWithLoader(this, () async { await sl().cancel( payment, - account: read().publicKey, + account: read().wallet, ); sl().linksCreated(); }); diff --git a/packages/espressocash_backend/espressocash_api/lib/src/client.dart b/packages/espressocash_backend/espressocash_api/lib/src/client.dart index 9d316dfb8a..c2e0577925 100644 --- a/packages/espressocash_backend/espressocash_api/lib/src/client.dart +++ b/packages/espressocash_backend/espressocash_api/lib/src/client.dart @@ -24,6 +24,11 @@ abstract class CryptopleaseClient { @Body() ReceivePaymentRequestDto request, ); + @POST('/cancelPayment') + Future cancelPayment( + @Body() CancelPaymentRequestDto request, + ); + @POST('/createDirectPayment') Future createDirectPayment( @Body() CreateDirectPaymentRequestDto request, diff --git a/packages/espressocash_backend/espressocash_api/lib/src/client.g.dart b/packages/espressocash_backend/espressocash_api/lib/src/client.g.dart index cae3c75ebc..9e094c67ce 100644 --- a/packages/espressocash_backend/espressocash_api/lib/src/client.g.dart +++ b/packages/espressocash_backend/espressocash_api/lib/src/client.g.dart @@ -92,6 +92,30 @@ class _CryptopleaseClient implements CryptopleaseClient { return value; } + @override + Future cancelPayment(request) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(request.toJson()); + final _result = await _dio.fetch>( + _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/cancelPayment', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); + final value = CancelPaymentResponseDto.fromJson(_result.data!); + return value; + } + @override Future createDirectPayment(request) async { const _extra = {}; diff --git a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.dart b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.dart index e6d632ce5f..562777ec2c 100644 --- a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.dart +++ b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.dart @@ -50,6 +50,29 @@ class ReceivePaymentResponseDto with _$ReceivePaymentResponseDto { _$ReceivePaymentResponseDtoFromJson(json); } +@freezed +class CancelPaymentRequestDto with _$CancelPaymentRequestDto { + const factory CancelPaymentRequestDto({ + required String senderAccount, + required String escrowAccount, + required Cluster cluster, + }) = _CancelPaymentRequestDto; + + factory CancelPaymentRequestDto.fromJson(Map json) => + _$CancelPaymentRequestDtoFromJson(json); +} + +@freezed +class CancelPaymentResponseDto with _$CancelPaymentResponseDto { + const factory CancelPaymentResponseDto({ + required String transaction, + required BigInt slot, + }) = _CancelPaymentResponseDto; + + factory CancelPaymentResponseDto.fromJson(Map json) => + _$CancelPaymentResponseDtoFromJson(json); +} + @freezed class CreateDirectPaymentRequestDto with _$CreateDirectPaymentRequestDto { const factory CreateDirectPaymentRequestDto({ diff --git a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.freezed.dart b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.freezed.dart index dfc9504ee8..fd9dbf2e6d 100644 --- a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.freezed.dart +++ b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.freezed.dart @@ -727,6 +727,349 @@ abstract class _ReceivePaymentResponseDto implements ReceivePaymentResponseDto { get copyWith => throw _privateConstructorUsedError; } +CancelPaymentRequestDto _$CancelPaymentRequestDtoFromJson( + Map json) { + return _CancelPaymentRequestDto.fromJson(json); +} + +/// @nodoc +mixin _$CancelPaymentRequestDto { + String get senderAccount => throw _privateConstructorUsedError; + String get escrowAccount => throw _privateConstructorUsedError; + Cluster get cluster => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $CancelPaymentRequestDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CancelPaymentRequestDtoCopyWith<$Res> { + factory $CancelPaymentRequestDtoCopyWith(CancelPaymentRequestDto value, + $Res Function(CancelPaymentRequestDto) then) = + _$CancelPaymentRequestDtoCopyWithImpl<$Res, CancelPaymentRequestDto>; + @useResult + $Res call({String senderAccount, String escrowAccount, Cluster cluster}); +} + +/// @nodoc +class _$CancelPaymentRequestDtoCopyWithImpl<$Res, + $Val extends CancelPaymentRequestDto> + implements $CancelPaymentRequestDtoCopyWith<$Res> { + _$CancelPaymentRequestDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? senderAccount = null, + Object? escrowAccount = null, + Object? cluster = null, + }) { + return _then(_value.copyWith( + senderAccount: null == senderAccount + ? _value.senderAccount + : senderAccount // ignore: cast_nullable_to_non_nullable + as String, + escrowAccount: null == escrowAccount + ? _value.escrowAccount + : escrowAccount // ignore: cast_nullable_to_non_nullable + as String, + cluster: null == cluster + ? _value.cluster + : cluster // ignore: cast_nullable_to_non_nullable + as Cluster, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_CancelPaymentRequestDtoCopyWith<$Res> + implements $CancelPaymentRequestDtoCopyWith<$Res> { + factory _$$_CancelPaymentRequestDtoCopyWith(_$_CancelPaymentRequestDto value, + $Res Function(_$_CancelPaymentRequestDto) then) = + __$$_CancelPaymentRequestDtoCopyWithImpl<$Res>; + @override + @useResult + $Res call({String senderAccount, String escrowAccount, Cluster cluster}); +} + +/// @nodoc +class __$$_CancelPaymentRequestDtoCopyWithImpl<$Res> + extends _$CancelPaymentRequestDtoCopyWithImpl<$Res, + _$_CancelPaymentRequestDto> + implements _$$_CancelPaymentRequestDtoCopyWith<$Res> { + __$$_CancelPaymentRequestDtoCopyWithImpl(_$_CancelPaymentRequestDto _value, + $Res Function(_$_CancelPaymentRequestDto) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? senderAccount = null, + Object? escrowAccount = null, + Object? cluster = null, + }) { + return _then(_$_CancelPaymentRequestDto( + senderAccount: null == senderAccount + ? _value.senderAccount + : senderAccount // ignore: cast_nullable_to_non_nullable + as String, + escrowAccount: null == escrowAccount + ? _value.escrowAccount + : escrowAccount // ignore: cast_nullable_to_non_nullable + as String, + cluster: null == cluster + ? _value.cluster + : cluster // ignore: cast_nullable_to_non_nullable + as Cluster, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_CancelPaymentRequestDto implements _CancelPaymentRequestDto { + const _$_CancelPaymentRequestDto( + {required this.senderAccount, + required this.escrowAccount, + required this.cluster}); + + factory _$_CancelPaymentRequestDto.fromJson(Map json) => + _$$_CancelPaymentRequestDtoFromJson(json); + + @override + final String senderAccount; + @override + final String escrowAccount; + @override + final Cluster cluster; + + @override + String toString() { + return 'CancelPaymentRequestDto(senderAccount: $senderAccount, escrowAccount: $escrowAccount, cluster: $cluster)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_CancelPaymentRequestDto && + (identical(other.senderAccount, senderAccount) || + other.senderAccount == senderAccount) && + (identical(other.escrowAccount, escrowAccount) || + other.escrowAccount == escrowAccount) && + (identical(other.cluster, cluster) || other.cluster == cluster)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, senderAccount, escrowAccount, cluster); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_CancelPaymentRequestDtoCopyWith<_$_CancelPaymentRequestDto> + get copyWith => + __$$_CancelPaymentRequestDtoCopyWithImpl<_$_CancelPaymentRequestDto>( + this, _$identity); + + @override + Map toJson() { + return _$$_CancelPaymentRequestDtoToJson( + this, + ); + } +} + +abstract class _CancelPaymentRequestDto implements CancelPaymentRequestDto { + const factory _CancelPaymentRequestDto( + {required final String senderAccount, + required final String escrowAccount, + required final Cluster cluster}) = _$_CancelPaymentRequestDto; + + factory _CancelPaymentRequestDto.fromJson(Map json) = + _$_CancelPaymentRequestDto.fromJson; + + @override + String get senderAccount; + @override + String get escrowAccount; + @override + Cluster get cluster; + @override + @JsonKey(ignore: true) + _$$_CancelPaymentRequestDtoCopyWith<_$_CancelPaymentRequestDto> + get copyWith => throw _privateConstructorUsedError; +} + +CancelPaymentResponseDto _$CancelPaymentResponseDtoFromJson( + Map json) { + return _CancelPaymentResponseDto.fromJson(json); +} + +/// @nodoc +mixin _$CancelPaymentResponseDto { + String get transaction => throw _privateConstructorUsedError; + BigInt get slot => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $CancelPaymentResponseDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CancelPaymentResponseDtoCopyWith<$Res> { + factory $CancelPaymentResponseDtoCopyWith(CancelPaymentResponseDto value, + $Res Function(CancelPaymentResponseDto) then) = + _$CancelPaymentResponseDtoCopyWithImpl<$Res, CancelPaymentResponseDto>; + @useResult + $Res call({String transaction, BigInt slot}); +} + +/// @nodoc +class _$CancelPaymentResponseDtoCopyWithImpl<$Res, + $Val extends CancelPaymentResponseDto> + implements $CancelPaymentResponseDtoCopyWith<$Res> { + _$CancelPaymentResponseDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? transaction = null, + Object? slot = null, + }) { + return _then(_value.copyWith( + transaction: null == transaction + ? _value.transaction + : transaction // ignore: cast_nullable_to_non_nullable + as String, + slot: null == slot + ? _value.slot + : slot // ignore: cast_nullable_to_non_nullable + as BigInt, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_CancelPaymentResponseDtoCopyWith<$Res> + implements $CancelPaymentResponseDtoCopyWith<$Res> { + factory _$$_CancelPaymentResponseDtoCopyWith( + _$_CancelPaymentResponseDto value, + $Res Function(_$_CancelPaymentResponseDto) then) = + __$$_CancelPaymentResponseDtoCopyWithImpl<$Res>; + @override + @useResult + $Res call({String transaction, BigInt slot}); +} + +/// @nodoc +class __$$_CancelPaymentResponseDtoCopyWithImpl<$Res> + extends _$CancelPaymentResponseDtoCopyWithImpl<$Res, + _$_CancelPaymentResponseDto> + implements _$$_CancelPaymentResponseDtoCopyWith<$Res> { + __$$_CancelPaymentResponseDtoCopyWithImpl(_$_CancelPaymentResponseDto _value, + $Res Function(_$_CancelPaymentResponseDto) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? transaction = null, + Object? slot = null, + }) { + return _then(_$_CancelPaymentResponseDto( + transaction: null == transaction + ? _value.transaction + : transaction // ignore: cast_nullable_to_non_nullable + as String, + slot: null == slot + ? _value.slot + : slot // ignore: cast_nullable_to_non_nullable + as BigInt, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_CancelPaymentResponseDto implements _CancelPaymentResponseDto { + const _$_CancelPaymentResponseDto( + {required this.transaction, required this.slot}); + + factory _$_CancelPaymentResponseDto.fromJson(Map json) => + _$$_CancelPaymentResponseDtoFromJson(json); + + @override + final String transaction; + @override + final BigInt slot; + + @override + String toString() { + return 'CancelPaymentResponseDto(transaction: $transaction, slot: $slot)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_CancelPaymentResponseDto && + (identical(other.transaction, transaction) || + other.transaction == transaction) && + (identical(other.slot, slot) || other.slot == slot)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, transaction, slot); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_CancelPaymentResponseDtoCopyWith<_$_CancelPaymentResponseDto> + get copyWith => __$$_CancelPaymentResponseDtoCopyWithImpl< + _$_CancelPaymentResponseDto>(this, _$identity); + + @override + Map toJson() { + return _$$_CancelPaymentResponseDtoToJson( + this, + ); + } +} + +abstract class _CancelPaymentResponseDto implements CancelPaymentResponseDto { + const factory _CancelPaymentResponseDto( + {required final String transaction, + required final BigInt slot}) = _$_CancelPaymentResponseDto; + + factory _CancelPaymentResponseDto.fromJson(Map json) = + _$_CancelPaymentResponseDto.fromJson; + + @override + String get transaction; + @override + BigInt get slot; + @override + @JsonKey(ignore: true) + _$$_CancelPaymentResponseDtoCopyWith<_$_CancelPaymentResponseDto> + get copyWith => throw _privateConstructorUsedError; +} + CreateDirectPaymentRequestDto _$CreateDirectPaymentRequestDtoFromJson( Map json) { return _CreateDirectPaymentRequestDto.fromJson(json); diff --git a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.g.dart b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.g.dart index 04570bac87..9e9bef9e46 100644 --- a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.g.dart +++ b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.g.dart @@ -73,6 +73,36 @@ Map _$$_ReceivePaymentResponseDtoToJson( 'slot': instance.slot.toString(), }; +_$_CancelPaymentRequestDto _$$_CancelPaymentRequestDtoFromJson( + Map json) => + _$_CancelPaymentRequestDto( + senderAccount: json['senderAccount'] as String, + escrowAccount: json['escrowAccount'] as String, + cluster: $enumDecode(_$ClusterEnumMap, json['cluster']), + ); + +Map _$$_CancelPaymentRequestDtoToJson( + _$_CancelPaymentRequestDto instance) => + { + 'senderAccount': instance.senderAccount, + 'escrowAccount': instance.escrowAccount, + 'cluster': _$ClusterEnumMap[instance.cluster]!, + }; + +_$_CancelPaymentResponseDto _$$_CancelPaymentResponseDtoFromJson( + Map json) => + _$_CancelPaymentResponseDto( + transaction: json['transaction'] as String, + slot: BigInt.parse(json['slot'] as String), + ); + +Map _$$_CancelPaymentResponseDtoToJson( + _$_CancelPaymentResponseDto instance) => + { + 'transaction': instance.transaction, + 'slot': instance.slot.toString(), + }; + _$_CreateDirectPaymentRequestDto _$$_CreateDirectPaymentRequestDtoFromJson( Map json) => _$_CreateDirectPaymentRequestDto( diff --git a/packages/espressocash_backend/lib/src/payments/cancel_payment.dart b/packages/espressocash_backend/lib/src/payments/cancel_payment.dart new file mode 100644 index 0000000000..5f5e94e25e --- /dev/null +++ b/packages/espressocash_backend/lib/src/payments/cancel_payment.dart @@ -0,0 +1,66 @@ +import 'package:dfunc/dfunc.dart'; +import 'package:espressocash_api/espressocash_api.dart'; +import 'package:espressocash_backend/src/payments/escrow_account.dart'; +import 'package:share_escrow/share_escrow.dart'; +import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; + +Future> cancelPaymentTx({ + required Ed25519HDPublicKey aEscrow, + required Ed25519HDPublicKey aSender, + required Ed25519HDPublicKey mint, + required Ed25519HDKeyPair platform, + required SolanaClient client, + required Commitment commitment, +}) async { + final escrow = await tryFetchEscrow( + address: aEscrow, + platform: platform, + mint: mint, + client: client, + commitment: commitment, + ); + + if (escrow == null) { + throw EspressoCashException( + error: EspressoCashError.invalidEscrowAccount, + ); + } + + final ataSender = await findAssociatedTokenAddress( + owner: aSender, + mint: mint, + ); + + final ataEscrow = await findAssociatedTokenAddress( + owner: aEscrow, + mint: mint, + ); + + final escrowIx = await EscrowInstruction.cancelEscrow( + escrowAccount: aEscrow, + depositorAccount: platform.publicKey, + senderAccount: aSender, + senderTokenAccount: ataSender, + vaultTokenAccount: ataEscrow, + ); + + final message = Message.only(escrowIx); + final latestBlockhash = + await client.rpcClient.getLatestBlockhash(commitment: commitment); + + final compiled = message.compile( + recentBlockhash: latestBlockhash.value.blockhash, + feePayer: platform.publicKey, + ); + + final tx = SignedTx( + compiledMessage: compiled, + signatures: [ + await platform.sign(compiled.toByteArray()), + Signature(List.filled(64, 0), publicKey: aSender), + ], + ); + + return Product2(tx, latestBlockhash.context.slot); +} diff --git a/packages/espressocash_backend/lib/src/payments/create_payment.dart b/packages/espressocash_backend/lib/src/payments/create_payment.dart index df16cf696d..dc35fe9757 100644 --- a/packages/espressocash_backend/lib/src/payments/create_payment.dart +++ b/packages/espressocash_backend/lib/src/payments/create_payment.dart @@ -1,5 +1,6 @@ import 'package:dfunc/dfunc.dart'; import 'package:espressocash_backend/src/constants.dart'; +import 'package:share_escrow/share_escrow.dart'; import 'package:solana/dto.dart' hide Instruction; import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; @@ -57,15 +58,6 @@ Future> createPaymentTx({ instructions.add(iCreateATA); - final iTransferAmount = TokenInstruction.transfer( - amount: amount, - source: ataSender, - destination: ataEscrow, - owner: aSender, - ); - - instructions.add(iTransferAmount); - final ataPlatform = await findAssociatedTokenAddress( owner: platform.publicKey, mint: mint, @@ -79,6 +71,17 @@ Future> createPaymentTx({ instructions.add(iTransferFee); + final escrowIx = await EscrowInstruction.initEscrow( + amount: amount, + escrowAccount: aEscrow, + senderAccount: aSender, + depositorAccount: platform.publicKey, + senderTokenAccount: ataSender, + vaultTokenAccount: ataEscrow, + ); + + instructions.add(escrowIx); + final message = Message(instructions: instructions); final latestBlockhash = await client.rpcClient.getLatestBlockhash(commitment: commitment); @@ -93,6 +96,7 @@ Future> createPaymentTx({ signatures: [ await platform.sign(compiled.toByteArray()), Signature(List.filled(64, 0), publicKey: aSender), + Signature(List.filled(64, 0), publicKey: aEscrow), ], ); diff --git a/packages/espressocash_backend/lib/src/payments/handler.dart b/packages/espressocash_backend/lib/src/payments/handler.dart index dbec10e178..184635d701 100644 --- a/packages/espressocash_backend/lib/src/payments/handler.dart +++ b/packages/espressocash_backend/lib/src/payments/handler.dart @@ -1,6 +1,7 @@ import 'package:dfunc/dfunc.dart'; import 'package:espressocash_api/espressocash_api.dart'; import 'package:espressocash_backend/src/constants.dart'; +import 'package:espressocash_backend/src/payments/cancel_payment.dart'; import 'package:espressocash_backend/src/payments/create_direct_payment.dart'; import 'package:espressocash_backend/src/payments/create_payment.dart'; import 'package:espressocash_backend/src/payments/receive_payment.dart'; @@ -12,6 +13,7 @@ import 'package:solana/solana.dart'; Handler paymentHandler() => (shelf_router.Router() ..post('/createPayment', createPaymentHandler) ..post('/receivePayment', receivePaymentHandler) + ..post('/cancelPayment', cancelPaymentHandler) ..post('/createDirectPayment', createDirectPaymentHandler) ..post('/getFees', getFeesHandler)) .call; @@ -76,6 +78,29 @@ Future receivePaymentHandler(Request request) async => }, ); +Future cancelPaymentHandler(Request request) async => + processRequest( + request, + CancelPaymentRequestDto.fromJson, + (data) async { + final cluster = data.cluster; + + final result = await cancelPaymentTx( + aSender: Ed25519HDPublicKey.fromBase58(data.senderAccount), + aEscrow: Ed25519HDPublicKey.fromBase58(data.escrowAccount), + mint: cluster.mint, + platform: await cluster.platformAccount, + client: cluster.solanaClient, + commitment: Commitment.confirmed, + ); + + return CancelPaymentResponseDto( + transaction: result.item1.encode(), + slot: result.item2, + ); + }, + ); + Future createDirectPaymentHandler(Request request) async => processRequest( diff --git a/packages/espressocash_backend/lib/src/payments/receive_payment.dart b/packages/espressocash_backend/lib/src/payments/receive_payment.dart index 0be223e635..ebc0d56e79 100644 --- a/packages/espressocash_backend/lib/src/payments/receive_payment.dart +++ b/packages/espressocash_backend/lib/src/payments/receive_payment.dart @@ -1,6 +1,7 @@ import 'package:dfunc/dfunc.dart'; import 'package:espressocash_api/espressocash_api.dart'; import 'package:espressocash_backend/src/payments/escrow_account.dart'; +import 'package:share_escrow/share_escrow.dart'; import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; @@ -55,22 +56,14 @@ Future> receivePaymentTx({ instructions.add(iCreateATA); } - final iTransfer = TokenInstruction.transfer( - amount: escrow.amount, - source: ataEscrow, - destination: ataReceiver, - owner: aEscrow, - ); - - instructions.add(iTransfer); - - final iCloseAccount = TokenInstruction.closeAccount( - accountToClose: ataEscrow, - destination: platform.publicKey, - owner: aEscrow, + final escrowIx = await EscrowInstruction.completeEscrow( + escrowAccount: aEscrow, + receiverTokenAccount: ataReceiver, + depositorAccount: platform.publicKey, + vaultTokenAccount: ataEscrow, ); - instructions.add(iCloseAccount); + instructions.add(escrowIx); final message = Message(instructions: instructions); final latestBlockhash = diff --git a/packages/espressocash_backend/pubspec.lock b/packages/espressocash_backend/pubspec.lock index e4a836d55f..206638b93e 100644 --- a/packages/espressocash_backend/pubspec.lock +++ b/packages/espressocash_backend/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + borsh: + dependency: transitive + description: + name: borsh + sha256: "58f762a29eb4bc5e24a733e19d5a51771c512f9734f6e538a17aae0a68cf30ee" + url: "https://pub.dev" + source: hosted + version: "0.3.1+2" borsh_annotation: dependency: transitive description: @@ -591,6 +599,13 @@ packages: url: "https://pub.dev" source: hosted version: "6.20.1" + share_escrow: + dependency: "direct main" + description: + path: share_escrow + relative: true + source: path + version: "1.0.0" shelf: dependency: "direct main" description: @@ -816,4 +831,4 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <3.0.0" + dart: ">=2.19.2 <3.0.0" diff --git a/packages/espressocash_backend/pubspec.yaml b/packages/espressocash_backend/pubspec.yaml index d86039f9f5..f0479307ee 100644 --- a/packages/espressocash_backend/pubspec.yaml +++ b/packages/espressocash_backend/pubspec.yaml @@ -20,6 +20,8 @@ dependencies: shelf_router: ^1.1.3 shelf_static: ^1.1.1 solana: ^0.29.0 + share_escrow: + path: share_escrow dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/espressocash_backend/share_escrow/lib/share_escrow.dart b/packages/espressocash_backend/share_escrow/lib/share_escrow.dart index 3aaefa2428..95599b3cd4 100644 --- a/packages/espressocash_backend/share_escrow/lib/share_escrow.dart +++ b/packages/espressocash_backend/share_escrow/lib/share_escrow.dart @@ -1,3 +1,4 @@ library share_escrow; -//TODO add exports +export 'src/instructions.dart'; +export 'src/models.dart'; diff --git a/packages/espressocash_backend/share_escrow/lib/src/instructions.dart b/packages/espressocash_backend/share_escrow/lib/src/instructions.dart index 0608c04ba2..3c18b177a5 100644 --- a/packages/espressocash_backend/share_escrow/lib/src/instructions.dart +++ b/packages/espressocash_backend/share_escrow/lib/src/instructions.dart @@ -34,7 +34,6 @@ class EscrowInstruction { static Future completeEscrow({ required Ed25519HDPublicKey escrowAccount, - required Ed25519HDPublicKey senderTokenAccount, required Ed25519HDPublicKey receiverTokenAccount, required Ed25519HDPublicKey depositorAccount, required Ed25519HDPublicKey vaultTokenAccount, @@ -45,7 +44,6 @@ class EscrowInstruction { accounts: [ AccountMeta.writeable(pubKey: escrowAccount, isSigner: true), AccountMeta.writeable(pubKey: depositorAccount, isSigner: true), - AccountMeta.writeable(pubKey: senderTokenAccount, isSigner: false), AccountMeta.writeable(pubKey: receiverTokenAccount, isSigner: false), AccountMeta.writeable(pubKey: vaultTokenAccount, isSigner: false), AccountMeta.readonly( diff --git a/packages/espressocash_backend/share_escrow/tests/escrow_test.dart b/packages/espressocash_backend/share_escrow/tests/escrow_test.dart index d0fa9c06fb..335db6028a 100644 --- a/packages/espressocash_backend/share_escrow/tests/escrow_test.dart +++ b/packages/espressocash_backend/share_escrow/tests/escrow_test.dart @@ -17,14 +17,8 @@ void main() { const initializerAmount = 1000; const sendAmount = 700; - const escrowAccountRent = 1893120; // calculated for escrow account len = 144 - const vaultTokenRent = 2039280; // calculated for token account len = 165 const airdropAmount = 1000000000; - const expectedDepositorBalanceAfterInit = airdropAmount - escrowAccountRent; - const expectedDepositorBalanceAfterComplete = - airdropAmount - escrowAccountRent + vaultTokenRent; - Future requestAirdrop(Ed25519HDKeyPair account) async { await client.requestAirdrop( address: account.publicKey, @@ -99,6 +93,13 @@ void main() { commitment: Commitment.confirmed, ); + final vault = await client.createAssociatedTokenAccount( + owner: escrow.publicKey, + mint: mint.address, + funder: depositor, + commitment: Commitment.confirmed, + ); + return Accounts( escrowAccount: escrow, senderAccount: sender, @@ -107,8 +108,7 @@ void main() { senderTokenAccount: sellerTokenAccount, receiverTokenAccount: await createAccount(client: client, owner: receiver, mint: mint), - vaultTokenAccount: - await createAccount(client: client, owner: sender, mint: mint), + vaultTokenAccount: Ed25519HDPublicKey.fromBase58(vault.pubkey), ); } @@ -180,7 +180,6 @@ void main() { escrowAccount: accounts.escrowAccount.publicKey, receiverTokenAccount: accounts.receiverTokenAccount, depositorAccount: accounts.depositorAccount.publicKey, - senderTokenAccount: accounts.senderTokenAccount, vaultTokenAccount: accounts.vaultTokenAccount, ); diff --git a/packages/espressocash_backend/test/payments/create_direct_payment_test.dart b/packages/espressocash_backend/test/payments/create_direct_payment_test.dart index 7b80e4101c..8a8d78466a 100644 --- a/packages/espressocash_backend/test/payments/create_direct_payment_test.dart +++ b/packages/espressocash_backend/test/payments/create_direct_payment_test.dart @@ -41,7 +41,7 @@ void main() { // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await sender.resign(payment.transaction); + final resignedTx = await payment.transaction.resign(sender); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), @@ -100,7 +100,7 @@ void main() { // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await sender.resign(payment.transaction); + final resignedTx = await payment.transaction.resign(sender); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), diff --git a/packages/espressocash_backend/test/payments/create_payment_test.dart b/packages/espressocash_backend/test/payments/create_payment_test.dart index b666c4bf04..b4220f99f8 100644 --- a/packages/espressocash_backend/test/payments/create_payment_test.dart +++ b/packages/espressocash_backend/test/payments/create_payment_test.dart @@ -1,3 +1,4 @@ +import 'package:dfunc/dfunc.dart'; import 'package:espressocash_backend/src/constants.dart'; import 'package:espressocash_backend/src/payments/create_payment.dart'; import 'package:solana/solana.dart'; @@ -43,7 +44,8 @@ Future main() async { // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await sender.resign(result.item1); + final resignedTx = + await result.item1.resign(sender).letAsync((p) => p.resign(escrow)); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), diff --git a/packages/espressocash_backend/test/payments/escrow_account_test.dart b/packages/espressocash_backend/test/payments/escrow_account_test.dart index a3874f85df..e8e8a07b23 100644 --- a/packages/espressocash_backend/test/payments/escrow_account_test.dart +++ b/packages/espressocash_backend/test/payments/escrow_account_test.dart @@ -78,7 +78,7 @@ void main() { // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await testData.sender.resign(result.item1); + final resignedTx = await result.item1.resign(testData.sender); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), diff --git a/packages/espressocash_backend/test/payments/receive_payment_test.dart b/packages/espressocash_backend/test/payments/receive_payment_test.dart index 4202ee4d6e..0bc92472d8 100644 --- a/packages/espressocash_backend/test/payments/receive_payment_test.dart +++ b/packages/espressocash_backend/test/payments/receive_payment_test.dart @@ -36,7 +36,7 @@ void main() { // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await testData.sender.resign(result.item1); + final resignedTx = await result.item1.resign(testData.sender); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), @@ -61,7 +61,7 @@ void main() { commitment: Commitment.confirmed, ); - final resignedReceiveTx = await escrowAccount.resign(receiveResult.item1); + final resignedReceiveTx = await receiveResult.item1.resign(escrowAccount); final signatureReceive = await client.rpcClient.sendTransaction( resignedReceiveTx.encode(), @@ -117,7 +117,7 @@ void main() { // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await testData.sender.resign(createPaymentResult.item1); + final resignedTx = await createPaymentResult.item1.resign(testData.sender); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), @@ -142,7 +142,7 @@ void main() { commitment: Commitment.confirmed, ); - final resignedReceiveTx = await escrowAccount.resign(receiveResult.item1); + final resignedReceiveTx = await receiveResult.item1.resign(escrowAccount); final signatureReceive = await client.rpcClient.sendTransaction( resignedReceiveTx.encode(), diff --git a/packages/espressocash_backend/test/payments/utils.dart b/packages/espressocash_backend/test/payments/utils.dart index c4e8f2d7ee..714da9e0d6 100644 --- a/packages/espressocash_backend/test/payments/utils.dart +++ b/packages/espressocash_backend/test/payments/utils.dart @@ -100,15 +100,16 @@ extension SolanaClientExt on SolanaClient { } } -extension Ed25519HDKeyPairExt on Ed25519HDKeyPair { - Future resign(SignedTx tx) async { - final compiledMessage = CompiledMessage(tx.compiledMessage.toByteArray()); - - return SignedTx( - signatures: tx.signatures.toList() - ..removeLast() - ..add(await sign(compiledMessage.toByteArray())), - compiledMessage: compiledMessage, - ); - } +extension SignedTxExt on SignedTx { + Future resign( + Ed25519HDKeyPair wallet, + ) async => + SignedTx( + signatures: signatures.toList() + ..setAll( + signatures.indexWhere((it) => it.publicKey == wallet.publicKey), [ + await wallet.sign(compiledMessage.toByteArray()), + ]), + compiledMessage: compiledMessage, + ); } From 2bdca4141f3720d669ca9f4cc1f2eed70665449c Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Tue, 21 Feb 2023 19:47:22 +0800 Subject: [PATCH 03/23] test upd --- .../lib/src/payments/create_payment.dart | 2 +- .../share_escrow/tests/utils/accounts.dart | 20 ----- .../share_escrow/tests/utils/config.dart | 9 -- .../test/payments/cancel_payment_test.dart | 82 +++++++++++++++++++ .../test/payments/create_payment_test.dart | 2 +- .../test/payments/escrow_account_test.dart | 7 +- .../test/payments/receive_payment_test.dart | 9 +- .../share_escrow}/escrow_test.dart | 5 +- .../share_escrow/utils.dart} | 19 +++++ 9 files changed, 117 insertions(+), 38 deletions(-) delete mode 100644 packages/espressocash_backend/share_escrow/tests/utils/accounts.dart delete mode 100644 packages/espressocash_backend/share_escrow/tests/utils/config.dart create mode 100644 packages/espressocash_backend/test/payments/cancel_payment_test.dart rename packages/espressocash_backend/{share_escrow/tests => test/share_escrow}/escrow_test.dart (98%) rename packages/espressocash_backend/{share_escrow/tests/utils/helper.dart => test/share_escrow/utils.dart} (67%) diff --git a/packages/espressocash_backend/lib/src/payments/create_payment.dart b/packages/espressocash_backend/lib/src/payments/create_payment.dart index dc35fe9757..c2e60c8a80 100644 --- a/packages/espressocash_backend/lib/src/payments/create_payment.dart +++ b/packages/espressocash_backend/lib/src/payments/create_payment.dart @@ -95,8 +95,8 @@ Future> createPaymentTx({ compiledMessage: compiled, signatures: [ await platform.sign(compiled.toByteArray()), - Signature(List.filled(64, 0), publicKey: aSender), Signature(List.filled(64, 0), publicKey: aEscrow), + Signature(List.filled(64, 0), publicKey: aSender), ], ); diff --git a/packages/espressocash_backend/share_escrow/tests/utils/accounts.dart b/packages/espressocash_backend/share_escrow/tests/utils/accounts.dart deleted file mode 100644 index a958675ee9..0000000000 --- a/packages/espressocash_backend/share_escrow/tests/utils/accounts.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:solana/solana.dart'; - -class Accounts { - Accounts({ - required this.escrowAccount, - required this.receiverAccount, - required this.senderTokenAccount, - required this.vaultTokenAccount, - required this.senderAccount, - required this.depositorAccount, - required this.receiverTokenAccount, - }); - final Ed25519HDKeyPair escrowAccount; - final Ed25519HDKeyPair receiverAccount; - final Ed25519HDKeyPair senderAccount; - final Ed25519HDKeyPair depositorAccount; - final Ed25519HDPublicKey senderTokenAccount; - final Ed25519HDPublicKey receiverTokenAccount; - final Ed25519HDPublicKey vaultTokenAccount; -} diff --git a/packages/espressocash_backend/share_escrow/tests/utils/config.dart b/packages/espressocash_backend/share_escrow/tests/utils/config.dart deleted file mode 100644 index d410468dd1..0000000000 --- a/packages/espressocash_backend/share_escrow/tests/utils/config.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:solana/solana.dart'; - -const devnetRpcUrl = 'http://127.0.0.1:8899'; -const devnetWebsocketUrl = 'ws://127.0.0.1:8900'; - -SolanaClient createTestSolanaClient() => SolanaClient( - rpcUrl: Uri.parse(devnetRpcUrl), - websocketUrl: Uri.parse(devnetWebsocketUrl), - ); diff --git a/packages/espressocash_backend/test/payments/cancel_payment_test.dart b/packages/espressocash_backend/test/payments/cancel_payment_test.dart new file mode 100644 index 0000000000..663faf23ba --- /dev/null +++ b/packages/espressocash_backend/test/payments/cancel_payment_test.dart @@ -0,0 +1,82 @@ +import 'package:dfunc/dfunc.dart'; +import 'package:espressocash_backend/src/constants.dart'; +import 'package:espressocash_backend/src/payments/cancel_payment.dart'; +import 'package:espressocash_backend/src/payments/create_payment.dart'; +import 'package:solana/solana.dart'; +import 'package:test/test.dart'; + +import '../config.dart'; +import 'utils.dart'; + +void main() { + final client = createTestSolanaClient(); + + /// Initial token amount for the sender after creation. + const senderInitialAmount = 500000; + + const transferAmount = 300000; + + test('cancels payment', () async { + final testData = await createTestData( + client: client, + senderInitialAmount: senderInitialAmount, + ); + + final escrowAccount = await Ed25519HDKeyPair.random(); + + final result = await createPaymentTx( + aSender: testData.sender.publicKey, + aEscrow: escrowAccount.publicKey, + mint: testData.mint, + amount: transferAmount, + client: client, + platform: testData.platform, + commitment: Commitment.confirmed, + ); + + // Sender and Escrow has to resign the transaction with their private key. The tx is + // already partially signed by the platform. + final resignedTx = await result.item1 + .resign(testData.sender) + .letAsync((tx) => tx.resign(escrowAccount)); + + final signature = await client.rpcClient.sendTransaction( + resignedTx.encode(), + preflightCommitment: Commitment.confirmed, + ); + + await client.waitForSignatureStatus( + signature, + status: Commitment.confirmed, + ); + + final receiveResult = await cancelPaymentTx( + aEscrow: escrowAccount.publicKey, + aSender: testData.sender.publicKey, + mint: testData.mint, + platform: testData.platform, + client: client, + commitment: Commitment.confirmed, + ); + + final resignedReceiveTx = await receiveResult.item1.resign(testData.sender); + + final signatureReceive = await client.rpcClient.sendTransaction( + resignedReceiveTx.encode(), + preflightCommitment: Commitment.confirmed, + ); + + await client.waitForSignatureStatus( + signatureReceive, + status: Commitment.confirmed, + ); + + final senderBalance = await client.getMintBalance( + testData.sender.publicKey, + mint: testData.mint, + ); + + // Receiver should get the expected amount. + expect(senderBalance, senderInitialAmount - shareableLinkPaymentFee); + }); +} diff --git a/packages/espressocash_backend/test/payments/create_payment_test.dart b/packages/espressocash_backend/test/payments/create_payment_test.dart index b4220f99f8..17d1bd8531 100644 --- a/packages/espressocash_backend/test/payments/create_payment_test.dart +++ b/packages/espressocash_backend/test/payments/create_payment_test.dart @@ -42,7 +42,7 @@ Future main() async { commitment: Commitment.confirmed, ); - // Sender has to resign the transaction with their private key. The tx is + // Sender and Escrow has to resign the transaction with their private key. The tx is // already partially signed by the platform. final resignedTx = await result.item1.resign(sender).letAsync((p) => p.resign(escrow)); diff --git a/packages/espressocash_backend/test/payments/escrow_account_test.dart b/packages/espressocash_backend/test/payments/escrow_account_test.dart index e8e8a07b23..6ec0169d7c 100644 --- a/packages/espressocash_backend/test/payments/escrow_account_test.dart +++ b/packages/espressocash_backend/test/payments/escrow_account_test.dart @@ -1,3 +1,4 @@ +import 'package:dfunc/dfunc.dart'; import 'package:espressocash_backend/src/payments/create_payment.dart'; import 'package:espressocash_backend/src/payments/escrow_account.dart'; import 'package:solana/solana.dart'; @@ -76,9 +77,11 @@ void main() { commitment: Commitment.confirmed, ); - // Sender has to resign the transaction with their private key. The tx is + // Sender and Escrow has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await result.item1.resign(testData.sender); + final resignedTx = await result.item1 + .resign(testData.sender) + .letAsync((tx) => tx.resign(escrowAccount)); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), diff --git a/packages/espressocash_backend/test/payments/receive_payment_test.dart b/packages/espressocash_backend/test/payments/receive_payment_test.dart index 0bc92472d8..9bf15d327c 100644 --- a/packages/espressocash_backend/test/payments/receive_payment_test.dart +++ b/packages/espressocash_backend/test/payments/receive_payment_test.dart @@ -1,3 +1,4 @@ +import 'package:dfunc/dfunc.dart'; import 'package:espressocash_backend/src/payments/create_payment.dart'; import 'package:espressocash_backend/src/payments/receive_payment.dart'; import 'package:solana/dto.dart'; @@ -36,7 +37,9 @@ void main() { // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await result.item1.resign(testData.sender); + final resignedTx = await result.item1 + .resign(testData.sender) + .letAsync((tx) => tx.resign(escrowAccount)); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), @@ -117,7 +120,9 @@ void main() { // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await createPaymentResult.item1.resign(testData.sender); + final resignedTx = await createPaymentResult.item1 + .resign(testData.sender) + .letAsync((tx) => tx.resign(escrowAccount)); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), diff --git a/packages/espressocash_backend/share_escrow/tests/escrow_test.dart b/packages/espressocash_backend/test/share_escrow/escrow_test.dart similarity index 98% rename from packages/espressocash_backend/share_escrow/tests/escrow_test.dart rename to packages/espressocash_backend/test/share_escrow/escrow_test.dart index 335db6028a..3b14fdab84 100644 --- a/packages/espressocash_backend/share_escrow/tests/escrow_test.dart +++ b/packages/espressocash_backend/test/share_escrow/escrow_test.dart @@ -4,9 +4,8 @@ import 'package:solana/dto.dart'; import 'package:solana/solana.dart'; import 'package:test/test.dart'; -import 'utils/accounts.dart'; -import 'utils/config.dart'; -import 'utils/helper.dart'; +import '../config.dart'; +import 'utils.dart'; void main() { final client = createTestSolanaClient(); diff --git a/packages/espressocash_backend/share_escrow/tests/utils/helper.dart b/packages/espressocash_backend/test/share_escrow/utils.dart similarity index 67% rename from packages/espressocash_backend/share_escrow/tests/utils/helper.dart rename to packages/espressocash_backend/test/share_escrow/utils.dart index d436431156..e550c43125 100644 --- a/packages/espressocash_backend/share_escrow/tests/utils/helper.dart +++ b/packages/espressocash_backend/test/share_escrow/utils.dart @@ -1,6 +1,25 @@ import 'package:solana/dto.dart'; import 'package:solana/solana.dart'; +class Accounts { + Accounts({ + required this.escrowAccount, + required this.receiverAccount, + required this.senderTokenAccount, + required this.vaultTokenAccount, + required this.senderAccount, + required this.depositorAccount, + required this.receiverTokenAccount, + }); + final Ed25519HDKeyPair escrowAccount; + final Ed25519HDKeyPair receiverAccount; + final Ed25519HDKeyPair senderAccount; + final Ed25519HDKeyPair depositorAccount; + final Ed25519HDPublicKey senderTokenAccount; + final Ed25519HDPublicKey receiverTokenAccount; + final Ed25519HDPublicKey vaultTokenAccount; +} + Future createAccount({ required SolanaClient client, required Ed25519HDKeyPair owner, From 36deb2a629745c34e48f4107bbeb89396012651a Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Wed, 22 Feb 2023 16:18:55 +0800 Subject: [PATCH 04/23] upd --- .../lib/core/transactions/tx_destinations.dart | 18 +++++++++++++----- .../src/bl/tx_ready_watcher.dart | 11 ++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/espressocash_app/lib/core/transactions/tx_destinations.dart b/packages/espressocash_app/lib/core/transactions/tx_destinations.dart index 0cddcbb1a3..75be520d20 100644 --- a/packages/espressocash_app/lib/core/transactions/tx_destinations.dart +++ b/packages/espressocash_app/lib/core/transactions/tx_destinations.dart @@ -2,14 +2,22 @@ import 'package:dfunc/dfunc.dart'; import 'package:solana/dto.dart'; extension TxDestinationsExt on ParsedTransaction { - /// Retrieves all destinations of a transaction - Iterable getDestinations() => message.instructions - .whereType() - .let((it) => it.map((ix) => ix.getDestination()).compact()); - String get id => signatures.first; } +extension MetaInnerInstructionExt on TransactionDetails { + /// Retrieves all destinations of a transaction + Iterable getInnerDestinations() => meta + .let((m) => m?.innerInstructions ?? []) + .map( + (e) => e.instructions + .whereType() + .let((it) => it.map((ix) => ix.getDestination()).compact()), + ) + .expand((e) => e) + .compact(); +} + extension on ParsedInstruction { String? getDestination() => mapOrNull( system: (it) => it.parsed.mapOrNull( diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart index 10ab2d8e5f..7781c6e246 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart @@ -30,9 +30,11 @@ class TxReadyWatcher { _repoSubscription = _repository.watchReady().distinct().listen((payments) async { for (final payment in payments) { - Future onSuccess(ParsedTransaction tx) async { + Future onSuccess(TransactionDetails txDetails) async { + final tx = txDetails.transaction as ParsedTransaction; final txId = tx.id; - final newStatus = await tx.getDestinations().let( + + final newStatus = await txDetails.getInnerDestinations().let( (accounts) => findAssociatedTokenAddress( owner: _userPublicKey, mint: payment.amount.cryptoCurrency.token.publicKey, @@ -63,7 +65,7 @@ class TxReadyWatcher { }); } - Stream _createStream({ + Stream _createStream({ required Ed25519HDPublicKey account, }) { Duration backoff = const Duration(seconds: 1); @@ -90,8 +92,7 @@ class TxReadyWatcher { .startWith(null) .flatMap(streamSignatures) .where((event) => event.length == 2) - .map((details) => details.first) - .map((tx) => tx.transaction as ParsedTransaction), + .map((details) => details.first), retryWhen, ); } From d2bf344a5e0aadaa8f9f91492e61fc5e669f2065 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Wed, 22 Feb 2023 18:40:40 +0800 Subject: [PATCH 05/23] analyzer fixes --- packages/espressocash_backend/pubspec.yaml | 4 ++-- packages/espressocash_backend/share_escrow/Makefile | 1 + .../espressocash_backend/share_escrow/lib/src/models.dart | 1 + packages/solana/lib/solana.dart | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 packages/espressocash_backend/share_escrow/Makefile diff --git a/packages/espressocash_backend/pubspec.yaml b/packages/espressocash_backend/pubspec.yaml index f0479307ee..bd80bf84a9 100644 --- a/packages/espressocash_backend/pubspec.yaml +++ b/packages/espressocash_backend/pubspec.yaml @@ -16,12 +16,12 @@ dependencies: json_annotation: ^4.7.0 mustache_template: ^2.0.0 sentry: ^6.20.1 + share_escrow: + path: share_escrow shelf: ^1.4.0 shelf_router: ^1.1.3 shelf_static: ^1.1.1 solana: ^0.29.0 - share_escrow: - path: share_escrow dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/espressocash_backend/share_escrow/Makefile b/packages/espressocash_backend/share_escrow/Makefile new file mode 100644 index 0000000000..041a142e28 --- /dev/null +++ b/packages/espressocash_backend/share_escrow/Makefile @@ -0,0 +1 @@ +include ../../../Makefile diff --git a/packages/espressocash_backend/share_escrow/lib/src/models.dart b/packages/espressocash_backend/share_escrow/lib/src/models.dart index d730e495f2..12c86857d5 100644 --- a/packages/espressocash_backend/share_escrow/lib/src/models.dart +++ b/packages/espressocash_backend/share_escrow/lib/src/models.dart @@ -2,6 +2,7 @@ import 'package:borsh_annotation/borsh_annotation.dart'; import 'package:solana/anchor.dart'; import 'package:solana/dto.dart'; import 'package:solana/solana.dart'; +// ignore: implementation_imports, Update when solana package is updated import 'package:solana/src/borsh_ext.dart'; part 'models.g.dart'; diff --git a/packages/solana/lib/solana.dart b/packages/solana/lib/solana.dart index 4b40a6257a..bae3e982d8 100644 --- a/packages/solana/lib/solana.dart +++ b/packages/solana/lib/solana.dart @@ -2,6 +2,7 @@ import 'package:solana/src/crypto/crypto.dart'; export 'dto.dart' show Commitment; export 'encoder.dart' show Message; +export 'src/borsh_ext.dart'; export 'src/crypto/crypto.dart'; export 'src/exceptions/exceptions.dart'; export 'src/helpers.dart'; From fa30b12bffeb8a72f7625728eb197b047532b273 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Wed, 22 Feb 2023 18:56:30 +0800 Subject: [PATCH 06/23] fix --- .../lib/src/metaplex/instructions/create_metadata_account.dart | 1 - packages/solana/lib/src/programs/token_program/raw_mint.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/solana/lib/src/metaplex/instructions/create_metadata_account.dart b/packages/solana/lib/src/metaplex/instructions/create_metadata_account.dart index d7dd621722..e526ff6581 100644 --- a/packages/solana/lib/src/metaplex/instructions/create_metadata_account.dart +++ b/packages/solana/lib/src/metaplex/instructions/create_metadata_account.dart @@ -2,7 +2,6 @@ import 'package:borsh_annotation/borsh_annotation.dart'; import 'package:solana/anchor.dart'; import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; -import 'package:solana/src/borsh_ext.dart'; import 'package:solana/src/metaplex/instructions/fixed_string.dart'; import 'package:solana/src/metaplex/utils.dart'; diff --git a/packages/solana/lib/src/programs/token_program/raw_mint.dart b/packages/solana/lib/src/programs/token_program/raw_mint.dart index 743929a691..91112a355a 100644 --- a/packages/solana/lib/src/programs/token_program/raw_mint.dart +++ b/packages/solana/lib/src/programs/token_program/raw_mint.dart @@ -1,6 +1,5 @@ import 'package:borsh_annotation/borsh_annotation.dart'; import 'package:solana/solana.dart'; -import 'package:solana/src/borsh_ext.dart'; part 'raw_mint.g.dart'; From 808fa749fbfe63a67dfa401dd4f9706df9e7ee48 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Thu, 2 Mar 2023 15:45:27 +0800 Subject: [PATCH 07/23] upd ci --- .github/workflows/check_pr.yml | 22 ++++++++++++------ .../test/bpf-programs/ec_shareable_links.so | Bin 0 -> 256544 bytes 2 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 packages/espressocash_backend/test/bpf-programs/ec_shareable_links.so diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml index 9cfb9a0a84..332abe0ec8 100644 --- a/.github/workflows/check_pr.yml +++ b/.github/workflows/check_pr.yml @@ -48,20 +48,28 @@ jobs: contents: write packages: read env: - DEVNET_RPC_URL: "http://solana:8899" - DEVNET_WEBSOCKET_URL: "ws://solana:8900" + DEVNET_RPC_URL: "http://localhost:8899" + DEVNET_WEBSOCKET_URL: "ws://localhost:8900" SCOPE: --scope="espressocash_backend" --scope="espressocash_api" --scope="jupiter_aggregator" + EC_LINKS_ID: "GHrMLBLnwGB8ypCWQnPeRzgHwePpUtSnY5ZSCCWzYmhC" + EC_LINKS_SO: "packages/espressocash_backend/test/bpf-programs/ec_shareable_links.so" container: image: ghcr.io/espresso-cash/flutter:3.7.0 - services: - solana: - image: solanalabs/solana:stable - options: --entrypoint="solana-test-validator" - steps: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v1.3 + - name: Install sudo package + run: apt update && apt install sudo + + - name: Start Solana Local Validator + uses: switchboard-xyz/solana-local-validator@v0.1 + with: + solana-version: ${{ matrix.solanaVersion }} + cluster: mainnet + args: + --bpf-program ${{ env.EC_LINKS_ID }} ${{ env.EC_LINKS_SO }} + - name: Activate melos run: make activate_utils diff --git a/packages/espressocash_backend/test/bpf-programs/ec_shareable_links.so b/packages/espressocash_backend/test/bpf-programs/ec_shareable_links.so new file mode 100644 index 0000000000000000000000000000000000000000..390a47f46c3efe2166a6ca0f32219221e04e8199 GIT binary patch literal 256544 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

OQj)^}_jbja2} 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℞6^L-z`7=APdOG5u6 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 I-?IGw2Zq*>WdHyG literal 0 HcmV?d00001 From bfc93ecc5246d1b67ab1ed2a5c3112208fe5ae94 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Thu, 2 Mar 2023 15:56:38 +0800 Subject: [PATCH 08/23] fix --- .github/workflows/check_pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml index 332abe0ec8..926936f5f6 100644 --- a/.github/workflows/check_pr.yml +++ b/.github/workflows/check_pr.yml @@ -65,7 +65,7 @@ jobs: - name: Start Solana Local Validator uses: switchboard-xyz/solana-local-validator@v0.1 with: - solana-version: ${{ matrix.solanaVersion }} + solana-version: stable cluster: mainnet args: --bpf-program ${{ env.EC_LINKS_ID }} ${{ env.EC_LINKS_SO }} From 027e969d4bbe069c71d8c605a54b11dbce009d51 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Wed, 15 Mar 2023 13:15:49 +0800 Subject: [PATCH 09/23] test updates --- .../share_escrow/lib/src/models.dart | 2 +- .../test/share_escrow/escrow_test.dart | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/espressocash_backend/share_escrow/lib/src/models.dart b/packages/espressocash_backend/share_escrow/lib/src/models.dart index 12c86857d5..47109b32b4 100644 --- a/packages/espressocash_backend/share_escrow/lib/src/models.dart +++ b/packages/espressocash_backend/share_escrow/lib/src/models.dart @@ -33,7 +33,7 @@ class EscrowDataAccount implements AnchorAccount { ); } - factory EscrowDataAccount.fromAccountData(AccountData accountData) { + factory EscrowDataAccount.fromAccountData(AccountData? accountData) { if (accountData is BinaryAccountData) { return EscrowDataAccount._fromBinary(accountData.data); } else { diff --git a/packages/espressocash_backend/test/share_escrow/escrow_test.dart b/packages/espressocash_backend/test/share_escrow/escrow_test.dart index 3b14fdab84..3071df6435 100644 --- a/packages/espressocash_backend/test/share_escrow/escrow_test.dart +++ b/packages/espressocash_backend/test/share_escrow/escrow_test.dart @@ -47,7 +47,11 @@ void main() { commitment: Commitment.confirmed, ); - return EscrowDataAccount.fromAccountData(account.value!.data!); + if (account.value == null) { + fail('Escrow account is null'); + } + + return EscrowDataAccount.fromAccountData(account.value?.data); } Future createEscrow(Accounts accounts) async { @@ -126,9 +130,13 @@ void main() { account: accounts.senderTokenAccount, ); + if (senderTokenAccount == null) { + fail('Sender token account is null'); + } + expect( initializerAmount - sendAmount, - int.tryParse(senderTokenAccount!.amount) ?? 0, + int.tryParse(senderTokenAccount.amount) ?? 0, ); final depositorTokenAccount = await getTokenAccountBalance( @@ -136,9 +144,13 @@ void main() { account: accounts.vaultTokenAccount, ); + if (depositorTokenAccount == null) { + fail('Depositor token account is null'); + } + expect( sendAmount, - int.tryParse(depositorTokenAccount!.amount) ?? 0, + int.tryParse(depositorTokenAccount.amount) ?? 0, ); }); From a55679dbe6a82b1c9677dc1f459cdfd681b891f7 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Fri, 17 Mar 2023 14:55:51 +0800 Subject: [PATCH 10/23] init: recover watcher --- .../lib/core/escrow_private_key.dart | 2 + packages/espressocash_app/lib/data/db/db.dart | 3 +- .../models/outgoing_split_key_payment.dart | 5 + .../outgoing_split_key_payments/module.dart | 7 + .../src/bl/payment_watcher.dart | 1 + .../src/bl/recover_pending_watcher.dart | 130 ++++++++++++++++++ .../src/bl/repository.dart | 21 +++ .../src/widgets/oskp_screen.dart | 2 + packages/solana/lib/src/rpc/dto/dto.dart | 1 + 9 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart diff --git a/packages/espressocash_app/lib/core/escrow_private_key.dart b/packages/espressocash_app/lib/core/escrow_private_key.dart index 42fdaffd4b..254553953b 100644 --- a/packages/espressocash_app/lib/core/escrow_private_key.dart +++ b/packages/espressocash_app/lib/core/escrow_private_key.dart @@ -3,6 +3,8 @@ import 'package:solana/solana.dart'; part 'escrow_private_key.freezed.dart'; +typedef EscrowPublicKey = Ed25519HDPublicKey; + @freezed class EscrowPrivateKey with _$EscrowPrivateKey { factory EscrowPrivateKey(List bytes) = _EscrowPrivateKey; diff --git a/packages/espressocash_app/lib/data/db/db.dart b/packages/espressocash_app/lib/data/db/db.dart index f93fe49592..f0df8b0cd2 100644 --- a/packages/espressocash_app/lib/data/db/db.dart +++ b/packages/espressocash_app/lib/data/db/db.dart @@ -26,7 +26,7 @@ class OutgoingTransferRows extends Table { Set>? get primaryKey => {id}; } -const int latestVersion = 34; +const int latestVersion = 35; const _tables = [ OutgoingTransferRows, @@ -149,6 +149,7 @@ class MyDatabase extends _$MyDatabase { await m.addColumn(oSKPRows, oSKPRows.resolvedAt); await m.addColumn(oSKPRows, oSKPRows.generatedLinksAt); } + //TODO add migration to OSKP }, ); diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart index d5477a47f9..c9cf25469d 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart @@ -1,5 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; import '../../../core/amount.dart'; import '../../../core/escrow_private_key.dart'; @@ -92,4 +93,8 @@ class OSKPStatus with _$OSKPStatus { required BigInt slot, required EscrowPrivateKey escrow, }) = OSKPStatusCancelTxSent; + + const factory OSKPStatus.recovered({ + required EscrowPublicKey escrow, + }) = OSKPStatusRecovered; } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart index 7ab2d04cd9..28bb2e29c4 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart @@ -8,6 +8,7 @@ import '../../core/balances/context_ext.dart'; import '../../di.dart'; import 'src/bl/cancel_tx_created_watcher.dart'; import 'src/bl/cancel_tx_sent_watcher.dart'; +import 'src/bl/recover_pending_watcher.dart'; import 'src/bl/repository.dart'; import 'src/bl/tx_confirmed_watcher.dart'; import 'src/bl/tx_created_watcher.dart'; @@ -57,6 +58,12 @@ class OSKPModule extends SingleChildStatelessWidget { ..call(onBalanceAffected: () => context.notifyBalanceAffected()), dispose: (_, value) => value.dispose(), ), + Provider( + lazy: false, + create: (context) => sl( + param1: context.read().wallet.publicKey, + )..init(), + ), ], child: LogoutListener( onLogout: (_) => sl().clear(), diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart index b20fd4a8d7..2b6d7c9f57 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart @@ -78,5 +78,6 @@ extension on OSKPStatus { cancelTxCreated: F, cancelTxFailure: F, cancelTxSent: F, + recovered: T, ); } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart new file mode 100644 index 0000000000..b9d925cc48 --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart @@ -0,0 +1,130 @@ +import 'package:collection/collection.dart'; +import 'package:dfunc/dfunc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:solana/dto.dart'; +import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../../core/amount.dart'; +import '../../../../core/currency.dart'; +import '../../../../core/tokens/token.dart'; +import '../../models/outgoing_split_key_payment.dart'; +import 'repository.dart'; + +@injectable +class RecoverPendingWatcher { + RecoverPendingWatcher( + this._client, + this._repository, { + @factoryParam required Ed25519HDPublicKey userPublicKey, + }) : _userPublicKey = userPublicKey; + + final SolanaClient _client; + final OSKPRepository _repository; + final Ed25519HDPublicKey _userPublicKey; + + Future init() async { + const fetchLimit = 100; + + final details = await _client.rpcClient.getTransactionsList( + limit: fetchLimit, + _userPublicKey, + encoding: Encoding.base64, + commitment: Commitment.confirmed, + ); + + final pendingEscrows = await _pendingEscrows(); + + for (final detail in details) { + final rawTx = detail.transaction as RawTransaction; + final tx = SignedTx.fromBytes(rawTx.data); + + final accounts = tx.compiledMessage.accountKeys; + + final hasInteractedWithEscrow = accounts.contains( + Ed25519HDPublicKey.fromBase58( + 'GHrMLBLnwGB8ypCWQnPeRzgHwePpUtSnY5ZSCCWzYmhC', + ), + ); + + if (hasInteractedWithEscrow) { + final escrow = accounts + .getRange(1, 2) + .where((e) => e != _userPublicKey) + .firstOrNull; + + if (escrow != null) { + if (pendingEscrows.contains(escrow.toBase58())) continue; + + final txList = await _client.rpcClient.getTransactionsList( + escrow, + limit: 2, + commitment: Commitment.confirmed, + encoding: Encoding.jsonParsed, + ); + + if (txList.length < 2) { + final id = const Uuid().v4(); + + final tx = txList.first; + + int amount = 0; + + for (final ix + in tx.meta?.innerInstructions?.last.instructions ?? []) { + if (ix is ParsedInstructionSplToken && + ix.parsed is ParsedSplTokenTransferInstruction) { + final parsed = ix.parsed as ParsedSplTokenTransferInstruction; + + amount = int.parse(parsed.info.amount); + } + } + + final timestamp = detail.blockTime?.let( + (it) => DateTime.fromMillisecondsSinceEpoch(it * 1000), + ) ?? + DateTime.now(); + + await _repository.save( + OutgoingSplitKeyPayment( + id: id, + amount: CryptoAmount( + value: amount, + cryptoCurrency: const CryptoCurrency(token: Token.usdc), + ), + status: OSKPStatus.recovered(escrow: escrow), + created: timestamp, + ), + ); + } + } + } + } + } + + Future> _pendingEscrows() async { + final pending = await _repository.watchPending().first; + + final List results = []; + + for (final p in pending) { + final escrowAddress = p.status.mapOrNull( + txCreated: (it) async => it.escrow.keyPair.then((v) => v.address), + txSent: (it) async => it.escrow.keyPair.then((v) => v.address), + txConfirmed: (it) async => it.escrow.keyPair.then((v) => v.address), + linksReady: (it) async => it.escrow.keyPair.then((v) => v.address), + cancelTxCreated: (it) async => it.escrow.keyPair.then((v) => v.address), + cancelTxFailure: (it) async => it.escrow.keyPair.then((v) => v.address), + cancelTxSent: (it) async => it.escrow.keyPair.then((v) => v.address), + recovered: (it) => it.escrow.toBase58(), + ); + + if (escrowAddress != null && escrowAddress is String) { + results.add(escrowAddress); + } + } + + return results; + } +} diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart index 2c0662ab5b..5197e54449 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart @@ -1,5 +1,6 @@ // ignore_for_file: avoid-non-null-assertion +import 'package:collection/collection.dart'; import 'package:dfunc/dfunc.dart'; import 'package:drift/drift.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -7,6 +8,7 @@ import 'package:injectable/injectable.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:solana/base58.dart'; import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; import '../../../../core/amount.dart'; import '../../../../core/currency.dart'; @@ -102,6 +104,12 @@ class OSKPRepository { OSKPStatusDto.cancelTxWaitFailure, ]); + Stream> watchPending() => _watchWithStatuses( + OSKPStatusDto.values.whereNot( + (e) => e == OSKPStatusDto.withdrawn || e == OSKPStatusDto.canceled, + ), + ); + Future clear() => _db.delete(_db.oSKPRows).go(); Stream> _watchWithStatuses( @@ -136,6 +144,7 @@ class OSKPRows extends Table with AmountMixin, EntityMixin { DateTimeColumn get generatedLinksAt => dateTime().nullable()(); DateTimeColumn get resolvedAt => dateTime().nullable()(); TextColumn get slot => text().nullable()(); + TextColumn get publicKey => text().nullable()(); } enum OSKPStatusDto { @@ -155,6 +164,7 @@ enum OSKPStatusDto { cancelTxSent, cancelTxSendFailure, cancelTxWaitFailure, + recovered, } extension OSKPRowExt on OSKPRow { @@ -269,6 +279,10 @@ extension on OSKPStatusDto { escrow: escrow!, slot: slot ?? BigInt.zero, ); + case OSKPStatusDto.recovered: + return OSKPStatus.recovered( + escrow: Ed25519HDPublicKey.fromBase58(row.publicKey!), + ); } } } @@ -293,6 +307,7 @@ extension on OutgoingSplitKeyPayment { slot: status.toSlot()?.toString(), generatedLinksAt: linksGeneratedAt, resolvedAt: status.toResolvedAt(), + publicKey: status.toPublicKey(), ); } @@ -308,6 +323,7 @@ extension on OSKPStatus { cancelTxCreated: always(OSKPStatusDto.cancelTxCreated), cancelTxFailure: always(OSKPStatusDto.cancelTxFailure), cancelTxSent: always(OSKPStatusDto.cancelTxSent), + recovered: always(OSKPStatusDto.recovered), ); String? toTx() => mapOrNull( @@ -344,6 +360,7 @@ extension on OSKPStatus { cancelTxCreated: (it) async => base58encode(it.escrow.bytes), cancelTxFailure: (it) async => base58encode(it.escrow.bytes), cancelTxSent: (it) async => base58encode(it.escrow.bytes), + recovered: (it) async => null, ); String? toLink1() => mapOrNull( @@ -374,4 +391,8 @@ extension on OSKPStatus { cancelTxCreated: (it) => it.slot, cancelTxSent: (it) => it.slot, ); + + String? toPublicKey() => mapOrNull( + recovered: (it) => it.escrow.toBase58(), + ); } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart index adfbc40334..f20f53e551 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart @@ -146,6 +146,7 @@ class _OSKPScreenState extends State { cancelTxCreated: always(CpStatusType.info), cancelTxFailure: always(CpStatusType.error), cancelTxSent: always(CpStatusType.info), + recovered: always(CpStatusType.info), ); final String? statusTitle = payment.status.mapOrNull( @@ -197,6 +198,7 @@ class _OSKPScreenState extends State { txConfirmed: always(1), linksReady: always(1), withdrawn: always(2), + recovered: always(0), ); final paymentInitiated = CpTimelineItem( diff --git a/packages/solana/lib/src/rpc/dto/dto.dart b/packages/solana/lib/src/rpc/dto/dto.dart index ed0adac0b3..119bede6ca 100644 --- a/packages/solana/lib/src/rpc/dto/dto.dart +++ b/packages/solana/lib/src/rpc/dto/dto.dart @@ -47,6 +47,7 @@ export 'recent_blockhash.dart'; export 'reward.dart'; export 'reward_type.dart'; export 'signature_status.dart'; +export 'simple_instruction.dart'; export 'simulate_transaction_accounts.dart'; export 'slot.dart'; export 'solana_version.dart'; From 8bb86441343d1e6b1e1cd30076c1844015bd6732 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Fri, 17 Mar 2023 15:09:23 +0800 Subject: [PATCH 11/23] upd cancel oksp --- packages/espressocash_app/lib/data/db/db.dart | 5 +++- .../models/outgoing_split_key_payment.dart | 6 ++-- .../src/bl/oskp_service.dart | 22 +++++++------- .../src/bl/payment_watcher.dart | 2 +- .../src/bl/recover_pending_watcher.dart | 29 ++++++++++--------- .../src/bl/repository.dart | 11 +++---- .../src/widgets/oskp_screen.dart | 5 ++-- 7 files changed, 42 insertions(+), 38 deletions(-) diff --git a/packages/espressocash_app/lib/data/db/db.dart b/packages/espressocash_app/lib/data/db/db.dart index f0df8b0cd2..0b32118c8a 100644 --- a/packages/espressocash_app/lib/data/db/db.dart +++ b/packages/espressocash_app/lib/data/db/db.dart @@ -149,7 +149,10 @@ class MyDatabase extends _$MyDatabase { await m.addColumn(oSKPRows, oSKPRows.resolvedAt); await m.addColumn(oSKPRows, oSKPRows.generatedLinksAt); } - //TODO add migration to OSKP + + if (from >= 16 && from < 35) { + await m.addColumn(oSKPRows, oSKPRows.publicKey); + } }, ); diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart index c9cf25469d..a4c31bbc1e 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart @@ -76,14 +76,14 @@ class OSKPStatus with _$OSKPStatus { const factory OSKPStatus.cancelTxCreated( SignedTx tx, { required BigInt slot, - required EscrowPrivateKey escrow, + required EscrowPublicKey escrow, }) = OSKPStatusCancelTxCreated; /// There was an error while creating the cancellation tx, or the tx was /// rejected. It's safe to recreate it. const factory OSKPStatus.cancelTxFailure({ required TxFailureReason reason, - required EscrowPrivateKey escrow, + required EscrowPublicKey escrow, }) = OSKPStatusCancelTxFailure; /// Cancellation tx was sent but not confirmed yet. It's not safe to recreate @@ -91,7 +91,7 @@ class OSKPStatus with _$OSKPStatus { const factory OSKPStatus.cancelTxSent( SignedTx tx, { required BigInt slot, - required EscrowPrivateKey escrow, + required EscrowPublicKey escrow, }) = OSKPStatusCancelTxSent; const factory OSKPStatus.recovered({ diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart index bc0e9fc3db..ad84097ebe 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart @@ -67,9 +67,11 @@ class OSKPService { timestamp: DateTime.now(), ); } else { - final escrow = status.mapOrNull( - linksReady: (it) => it.escrow, - cancelTxFailure: (it) => it.escrow, + final escrow = await status.mapOrNull( + linksReady: (it) async => it.escrow.keyPair + .then((e) => Ed25519HDPublicKey.fromBase58(e.address)), + recovered: (it) async => it.escrow, + cancelTxFailure: (it) async => it.escrow, ); if (escrow == null) { @@ -77,9 +79,7 @@ class OSKPService { } newStatus = await _createCancelTx( - escrow: await Ed25519HDKeyPair.fromPrivateKeyBytes( - privateKey: escrow.bytes, - ), + escrow: escrow, account: account, ); } @@ -121,15 +121,13 @@ class OSKPService { } Future _createCancelTx({ - required Ed25519HDKeyPair escrow, + required EscrowPublicKey escrow, required ECWallet account, }) async { - final privateKey = await EscrowPrivateKey.fromKeyPair(escrow); - try { final dto = CancelPaymentRequestDto( senderAccount: account.address, - escrowAccount: escrow.address, + escrowAccount: escrow.toBase58(), cluster: apiCluster, ); @@ -141,12 +139,12 @@ class OSKPService { return OSKPStatus.cancelTxCreated( tx, - escrow: privateKey, + escrow: escrow, slot: response.slot, ); } on Exception { return OSKPStatus.cancelTxFailure( - escrow: privateKey, + escrow: escrow, reason: TxFailureReason.creatingFailure, ); } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart index 2b6d7c9f57..13fb5200cb 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart @@ -78,6 +78,6 @@ extension on OSKPStatus { cancelTxCreated: F, cancelTxFailure: F, cancelTxSent: F, - recovered: T, + recovered: F, ); } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart index b9d925cc48..c457ad3a7a 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart @@ -8,6 +8,7 @@ import 'package:uuid/uuid.dart'; import '../../../../core/amount.dart'; import '../../../../core/currency.dart'; +import '../../../../core/escrow_private_key.dart'; import '../../../../core/tokens/token.dart'; import '../../models/outgoing_split_key_payment.dart'; import 'repository.dart'; @@ -55,7 +56,7 @@ class RecoverPendingWatcher { .firstOrNull; if (escrow != null) { - if (pendingEscrows.contains(escrow.toBase58())) continue; + if (pendingEscrows.contains(escrow)) continue; final txList = await _client.rpcClient.getTransactionsList( escrow, @@ -103,25 +104,25 @@ class RecoverPendingWatcher { } } - Future> _pendingEscrows() async { + Future> _pendingEscrows() async { final pending = await _repository.watchPending().first; - final List results = []; + final List results = []; for (final p in pending) { - final escrowAddress = p.status.mapOrNull( - txCreated: (it) async => it.escrow.keyPair.then((v) => v.address), - txSent: (it) async => it.escrow.keyPair.then((v) => v.address), - txConfirmed: (it) async => it.escrow.keyPair.then((v) => v.address), - linksReady: (it) async => it.escrow.keyPair.then((v) => v.address), - cancelTxCreated: (it) async => it.escrow.keyPair.then((v) => v.address), - cancelTxFailure: (it) async => it.escrow.keyPair.then((v) => v.address), - cancelTxSent: (it) async => it.escrow.keyPair.then((v) => v.address), - recovered: (it) => it.escrow.toBase58(), + final escrow = await p.status.mapOrNull( + txCreated: (it) async => it.escrow.keyPair.then((v) => v.publicKey), + txSent: (it) async => it.escrow.keyPair.then((v) => v.publicKey), + txConfirmed: (it) async => it.escrow.keyPair.then((v) => v.publicKey), + linksReady: (it) async => it.escrow.keyPair.then((v) => v.publicKey), + cancelTxCreated: (it) async => it.escrow, + cancelTxFailure: (it) async => it.escrow, + cancelTxSent: (it) async => it.escrow, + recovered: (it) async => it.escrow, ); - if (escrowAddress != null && escrowAddress is String) { - results.add(escrowAddress); + if (escrow != null) { + results.add(escrow); } } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart index 5197e54449..94535f1cf2 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart @@ -187,6 +187,7 @@ extension on OSKPStatusDto { final txId = row.txId; final withdrawTxId = row.withdrawTxId; final escrow = row.privateKey?.let(base58decode).let(EscrowPrivateKey.new); + final escrowPk = await escrow?.keyPair.then((e) => e.publicKey); final link1 = row.link1?.let(Uri.parse); final link2 = row.link2?.let(Uri.parse); final link3 = row.link3?.let(Uri.tryParse); @@ -253,30 +254,30 @@ extension on OSKPStatusDto { case OSKPStatusDto.cancelTxCreated: return OSKPStatus.cancelTxCreated( cancelTx!, - escrow: escrow!, + escrow: escrowPk!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.cancelTxFailure: return OSKPStatus.cancelTxFailure( - escrow: escrow!, + escrow: escrowPk!, reason: row.txFailureReason ?? TxFailureReason.unknown, ); case OSKPStatusDto.cancelTxSent: return OSKPStatus.cancelTxSent( cancelTx!, - escrow: escrow!, + escrow: escrowPk!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.cancelTxSendFailure: return OSKPStatus.cancelTxCreated( cancelTx!, - escrow: escrow!, + escrow: escrowPk!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.cancelTxWaitFailure: return OSKPStatus.cancelTxSent( cancelTx!, - escrow: escrow!, + escrow: escrowPk!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.recovered: diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart index f20f53e551..e758d6a3b8 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart @@ -132,6 +132,7 @@ class _OSKPScreenState extends State { onPressed: onCancel, ), ], + recovered: (s) => [cancelButton], orElse: () => const [], ); @@ -198,7 +199,7 @@ class _OSKPScreenState extends State { txConfirmed: always(1), linksReady: always(1), withdrawn: always(2), - recovered: always(0), + recovered: always(1), ); final paymentInitiated = CpTimelineItem( @@ -240,7 +241,7 @@ class _OSKPScreenState extends State { normalItems; final animated = timelineStatus == CpTimelineStatus.inProgress && - payment.status.maybeMap(orElse: T, linksReady: F); + payment.status.maybeMap(orElse: T, linksReady: F, recovered: F); return StatusScreen( onBackButtonPressed: () => context.router.pop(), From a105a4c398b39bd18bdba282f0931f212067f680 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Fri, 17 Mar 2023 17:42:12 +0800 Subject: [PATCH 12/23] upd --- .../src/bl/recover_pending_watcher.dart | 1 + .../src/bl/repository.dart | 1 + .../src/bl/tx_ready_watcher.dart | 14 +++++++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart index c457ad3a7a..b0afeeda64 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart @@ -96,6 +96,7 @@ class RecoverPendingWatcher { ), status: OSKPStatus.recovered(escrow: escrow), created: timestamp, + linksGeneratedAt: timestamp, ), ); } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart index 94535f1cf2..583916339e 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart @@ -73,6 +73,7 @@ class OSKPRepository { OSKPStatusDto.cancelTxSent, OSKPStatusDto.cancelTxWaitFailure, OSKPStatusDto.cancelTxFailure, + OSKPStatusDto.recovered, ]); Stream> watchTxCreated() => diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart index 6d531b6a83..c61798ddc4 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart @@ -58,13 +58,21 @@ class TxReadyWatcher { } final status = payment.status; - if (status is! OSKPStatusLinksReady) continue; + if (status is! OSKPStatusLinksReady && status is! OSKPStatusRecovered) { + continue; + } + + final escrowAccount = await status.mapOrNull( + linksReady: (it) async => it.escrow.keyPair + .then((e) => Ed25519HDPublicKey.fromBase58(e.address)), + recovered: (it) async => it.escrow, + ); - final escrowAccount = await status.escrow.keyPair; + if (escrowAccount == null) continue; if (!_subscriptions.containsKey(payment.id)) { _subscriptions[payment.id] = - _createStream(account: escrowAccount.publicKey).listen(onSuccess); + _createStream(account: escrowAccount).listen(onSuccess); } } }); From f0c990320c7922376a2f2fb97de2ef2574f9bd53 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Fri, 17 Mar 2023 18:07:36 +0800 Subject: [PATCH 13/23] added comments --- .../src/bl/recover_pending_watcher.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart index b0afeeda64..be88385fa6 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart @@ -41,8 +41,8 @@ class RecoverPendingWatcher { final rawTx = detail.transaction as RawTransaction; final tx = SignedTx.fromBytes(rawTx.data); + // Check if the transaction has interacted with the escrow smart contract final accounts = tx.compiledMessage.accountKeys; - final hasInteractedWithEscrow = accounts.contains( Ed25519HDPublicKey.fromBase58( 'GHrMLBLnwGB8ypCWQnPeRzgHwePpUtSnY5ZSCCWzYmhC', @@ -50,6 +50,8 @@ class RecoverPendingWatcher { ); if (hasInteractedWithEscrow) { + // Find the escrow address from accounts. It should either be in index 1 or 2. + // Index 0 is the platforms account, index 1 or 2 should either be the user or the escrow. final escrow = accounts .getRange(1, 2) .where((e) => e != _userPublicKey) From 788ab4fdbc79b087f89f0aa73b72ac492ed4e994 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Tue, 4 Apr 2023 17:28:44 +0800 Subject: [PATCH 14/23] upd --- .github/workflows/check_pr.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml index 584e7400a6..e3d97dc700 100644 --- a/.github/workflows/check_pr.yml +++ b/.github/workflows/check_pr.yml @@ -54,7 +54,8 @@ jobs: DEVNET_RPC_URL: "http://localhost:8899" DEVNET_WEBSOCKET_URL: "ws://localhost:8900" SCOPE: --scope="espressocash_backend" --scope="espressocash_api" --scope="jupiter_aggregator" - + EC_LINKS_ID: "GHrMLBLnwGB8ypCWQnPeRzgHwePpUtSnY5ZSCCWzYmhC" + EC_LINKS_SO: "packages/espressocash_backend/test/bpf-programs/ec_shareable_links.so" steps: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v1.3 @@ -73,6 +74,8 @@ jobs: with: solana-version: stable cluster: mainnet + args: + --bpf-program ${{ env.EC_LINKS_ID }} ${{ env.EC_LINKS_SO }} - name: Activate melos run: make activate_utils From c4c9ccf0cf99257bda6f0d6a8825ff0dc5f5e833 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Mon, 24 Apr 2023 19:17:19 +0800 Subject: [PATCH 15/23] revert --- packages/espressocash_app/lib/data/db/db.dart | 1 - .../espressocash_api/lib/src/client.dart | 5 - .../espressocash_api/lib/src/client.g.dart | 24 -- .../lib/src/dto/create_payment.dart | 23 -- .../lib/src/payments/cancel_payment.dart | 66 ------ .../lib/src/payments/create_payment.dart | 22 +- .../lib/src/payments/handler.dart | 25 --- .../lib/src/payments/receive_payment.dart | 21 +- packages/espressocash_backend/pubspec.lock | 2 +- .../share_escrow/.gitignore | 7 - .../share_escrow/CHANGELOG.md | 3 - .../share_escrow/Makefile | 1 - .../share_escrow/README.md | 39 ---- .../share_escrow/analysis_options.yaml | 1 - .../share_escrow/lib/share_escrow.dart | 4 - .../share_escrow/lib/src/instructions.dart | 97 -------- .../share_escrow/lib/src/models.dart | 86 ------- .../share_escrow/lib/src/models.g.dart | 111 --------- .../share_escrow/pubspec.yaml | 18 -- .../test/payments/cancel_payment_test.dart | 82 ------- .../payments/create_direct_payment_test.dart | 80 +++---- .../test/payments/create_payment_test.dart | 6 +- .../test/payments/escrow_account_test.dart | 7 +- .../test/payments/receive_payment_test.dart | 13 +- .../test/payments/utils.dart | 23 +- .../test/share_escrow/escrow_test.dart | 211 ------------------ .../test/share_escrow/utils.dart | 70 ------ 27 files changed, 69 insertions(+), 979 deletions(-) delete mode 100644 packages/espressocash_backend/lib/src/payments/cancel_payment.dart delete mode 100644 packages/espressocash_backend/share_escrow/.gitignore delete mode 100644 packages/espressocash_backend/share_escrow/CHANGELOG.md delete mode 100644 packages/espressocash_backend/share_escrow/Makefile delete mode 100644 packages/espressocash_backend/share_escrow/README.md delete mode 100644 packages/espressocash_backend/share_escrow/analysis_options.yaml delete mode 100644 packages/espressocash_backend/share_escrow/lib/share_escrow.dart delete mode 100644 packages/espressocash_backend/share_escrow/lib/src/instructions.dart delete mode 100644 packages/espressocash_backend/share_escrow/lib/src/models.dart delete mode 100644 packages/espressocash_backend/share_escrow/lib/src/models.g.dart delete mode 100644 packages/espressocash_backend/share_escrow/pubspec.yaml delete mode 100644 packages/espressocash_backend/test/payments/cancel_payment_test.dart delete mode 100644 packages/espressocash_backend/test/share_escrow/escrow_test.dart delete mode 100644 packages/espressocash_backend/test/share_escrow/utils.dart diff --git a/packages/espressocash_app/lib/data/db/db.dart b/packages/espressocash_app/lib/data/db/db.dart index 1f323a1c52..06e4aa6583 100644 --- a/packages/espressocash_app/lib/data/db/db.dart +++ b/packages/espressocash_app/lib/data/db/db.dart @@ -140,7 +140,6 @@ class MyDatabase extends _$MyDatabase { await m.addColumn(oSKPRows, oSKPRows.resolvedAt); await m.addColumn(oSKPRows, oSKPRows.generatedLinksAt); } - if (from >= 16 && from < 35) { await m.addColumn(oSKPRows, oSKPRows.apiVersion); } diff --git a/packages/espressocash_backend/espressocash_api/lib/src/client.dart b/packages/espressocash_backend/espressocash_api/lib/src/client.dart index 2461918944..a03f8718fe 100644 --- a/packages/espressocash_backend/espressocash_api/lib/src/client.dart +++ b/packages/espressocash_backend/espressocash_api/lib/src/client.dart @@ -24,11 +24,6 @@ abstract class CryptopleaseClient { @Body() ReceivePaymentRequestDto request, ); - @POST('/cancelPayment') - Future cancelPayment( - @Body() CancelPaymentRequestDto request, - ); - @POST('/createDirectPayment') Future createDirectPayment( @Body() CreateDirectPaymentRequestDto request, diff --git a/packages/espressocash_backend/espressocash_api/lib/src/client.g.dart b/packages/espressocash_backend/espressocash_api/lib/src/client.g.dart index a5bd0d3a2a..a8acaad5cd 100644 --- a/packages/espressocash_backend/espressocash_api/lib/src/client.g.dart +++ b/packages/espressocash_backend/espressocash_api/lib/src/client.g.dart @@ -92,30 +92,6 @@ class _CryptopleaseClient implements CryptopleaseClient { return value; } - @override - Future cancelPayment(request) async { - const _extra = {}; - final queryParameters = {}; - final _headers = {}; - final _data = {}; - _data.addAll(request.toJson()); - final _result = await _dio.fetch>( - _setStreamType(Options( - method: 'POST', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - '/cancelPayment', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); - final value = CancelPaymentResponseDto.fromJson(_result.data!); - return value; - } - @override Future createDirectPayment(request) async { const _extra = {}; diff --git a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.dart b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.dart index 3e26d3206a..f569d445a2 100644 --- a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.dart +++ b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.dart @@ -50,29 +50,6 @@ class ReceivePaymentResponseDto with _$ReceivePaymentResponseDto { _$ReceivePaymentResponseDtoFromJson(json); } -@freezed -class CancelPaymentRequestDto with _$CancelPaymentRequestDto { - const factory CancelPaymentRequestDto({ - required String senderAccount, - required String escrowAccount, - required Cluster cluster, - }) = _CancelPaymentRequestDto; - - factory CancelPaymentRequestDto.fromJson(Map json) => - _$CancelPaymentRequestDtoFromJson(json); -} - -@freezed -class CancelPaymentResponseDto with _$CancelPaymentResponseDto { - const factory CancelPaymentResponseDto({ - required String transaction, - required BigInt slot, - }) = _CancelPaymentResponseDto; - - factory CancelPaymentResponseDto.fromJson(Map json) => - _$CancelPaymentResponseDtoFromJson(json); -} - @freezed class CreateDirectPaymentRequestDto with _$CreateDirectPaymentRequestDto { const factory CreateDirectPaymentRequestDto({ diff --git a/packages/espressocash_backend/lib/src/payments/cancel_payment.dart b/packages/espressocash_backend/lib/src/payments/cancel_payment.dart deleted file mode 100644 index 5f5e94e25e..0000000000 --- a/packages/espressocash_backend/lib/src/payments/cancel_payment.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:dfunc/dfunc.dart'; -import 'package:espressocash_api/espressocash_api.dart'; -import 'package:espressocash_backend/src/payments/escrow_account.dart'; -import 'package:share_escrow/share_escrow.dart'; -import 'package:solana/encoder.dart'; -import 'package:solana/solana.dart'; - -Future> cancelPaymentTx({ - required Ed25519HDPublicKey aEscrow, - required Ed25519HDPublicKey aSender, - required Ed25519HDPublicKey mint, - required Ed25519HDKeyPair platform, - required SolanaClient client, - required Commitment commitment, -}) async { - final escrow = await tryFetchEscrow( - address: aEscrow, - platform: platform, - mint: mint, - client: client, - commitment: commitment, - ); - - if (escrow == null) { - throw EspressoCashException( - error: EspressoCashError.invalidEscrowAccount, - ); - } - - final ataSender = await findAssociatedTokenAddress( - owner: aSender, - mint: mint, - ); - - final ataEscrow = await findAssociatedTokenAddress( - owner: aEscrow, - mint: mint, - ); - - final escrowIx = await EscrowInstruction.cancelEscrow( - escrowAccount: aEscrow, - depositorAccount: platform.publicKey, - senderAccount: aSender, - senderTokenAccount: ataSender, - vaultTokenAccount: ataEscrow, - ); - - final message = Message.only(escrowIx); - final latestBlockhash = - await client.rpcClient.getLatestBlockhash(commitment: commitment); - - final compiled = message.compile( - recentBlockhash: latestBlockhash.value.blockhash, - feePayer: platform.publicKey, - ); - - final tx = SignedTx( - compiledMessage: compiled, - signatures: [ - await platform.sign(compiled.toByteArray()), - Signature(List.filled(64, 0), publicKey: aSender), - ], - ); - - return Product2(tx, latestBlockhash.context.slot); -} diff --git a/packages/espressocash_backend/lib/src/payments/create_payment.dart b/packages/espressocash_backend/lib/src/payments/create_payment.dart index c2e60c8a80..df16cf696d 100644 --- a/packages/espressocash_backend/lib/src/payments/create_payment.dart +++ b/packages/espressocash_backend/lib/src/payments/create_payment.dart @@ -1,6 +1,5 @@ import 'package:dfunc/dfunc.dart'; import 'package:espressocash_backend/src/constants.dart'; -import 'package:share_escrow/share_escrow.dart'; import 'package:solana/dto.dart' hide Instruction; import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; @@ -58,6 +57,15 @@ Future> createPaymentTx({ instructions.add(iCreateATA); + final iTransferAmount = TokenInstruction.transfer( + amount: amount, + source: ataSender, + destination: ataEscrow, + owner: aSender, + ); + + instructions.add(iTransferAmount); + final ataPlatform = await findAssociatedTokenAddress( owner: platform.publicKey, mint: mint, @@ -71,17 +79,6 @@ Future> createPaymentTx({ instructions.add(iTransferFee); - final escrowIx = await EscrowInstruction.initEscrow( - amount: amount, - escrowAccount: aEscrow, - senderAccount: aSender, - depositorAccount: platform.publicKey, - senderTokenAccount: ataSender, - vaultTokenAccount: ataEscrow, - ); - - instructions.add(escrowIx); - final message = Message(instructions: instructions); final latestBlockhash = await client.rpcClient.getLatestBlockhash(commitment: commitment); @@ -95,7 +92,6 @@ Future> createPaymentTx({ compiledMessage: compiled, signatures: [ await platform.sign(compiled.toByteArray()), - Signature(List.filled(64, 0), publicKey: aEscrow), Signature(List.filled(64, 0), publicKey: aSender), ], ); diff --git a/packages/espressocash_backend/lib/src/payments/handler.dart b/packages/espressocash_backend/lib/src/payments/handler.dart index a076027fe5..c2fcb5df0a 100644 --- a/packages/espressocash_backend/lib/src/payments/handler.dart +++ b/packages/espressocash_backend/lib/src/payments/handler.dart @@ -1,7 +1,6 @@ import 'package:dfunc/dfunc.dart'; import 'package:espressocash_api/espressocash_api.dart'; import 'package:espressocash_backend/src/constants.dart'; -import 'package:espressocash_backend/src/payments/cancel_payment.dart'; import 'package:espressocash_backend/src/payments/create_direct_payment.dart'; import 'package:espressocash_backend/src/payments/create_payment.dart'; import 'package:espressocash_backend/src/payments/receive_payment.dart'; @@ -13,7 +12,6 @@ import 'package:solana/solana.dart'; Handler paymentHandler() => (shelf_router.Router() ..post('/createPayment', createPaymentHandler) ..post('/receivePayment', receivePaymentHandler) - ..post('/cancelPayment', cancelPaymentHandler) ..post('/createDirectPayment', createDirectPaymentHandler) ..post('/getFees', getFeesHandler)) .call; @@ -79,29 +77,6 @@ Future receivePaymentHandler(Request request) async => }, ); -Future cancelPaymentHandler(Request request) async => - processRequest( - request, - CancelPaymentRequestDto.fromJson, - (data) async { - final cluster = data.cluster; - - final result = await cancelPaymentTx( - aSender: Ed25519HDPublicKey.fromBase58(data.senderAccount), - aEscrow: Ed25519HDPublicKey.fromBase58(data.escrowAccount), - mint: cluster.mint, - platform: await cluster.platformAccount, - client: cluster.solanaClient, - commitment: Commitment.confirmed, - ); - - return CancelPaymentResponseDto( - transaction: result.item1.encode(), - slot: result.item2, - ); - }, - ); - Future createDirectPaymentHandler(Request request) async => processRequest( diff --git a/packages/espressocash_backend/lib/src/payments/receive_payment.dart b/packages/espressocash_backend/lib/src/payments/receive_payment.dart index ebc0d56e79..0be223e635 100644 --- a/packages/espressocash_backend/lib/src/payments/receive_payment.dart +++ b/packages/espressocash_backend/lib/src/payments/receive_payment.dart @@ -1,7 +1,6 @@ import 'package:dfunc/dfunc.dart'; import 'package:espressocash_api/espressocash_api.dart'; import 'package:espressocash_backend/src/payments/escrow_account.dart'; -import 'package:share_escrow/share_escrow.dart'; import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; @@ -56,14 +55,22 @@ Future> receivePaymentTx({ instructions.add(iCreateATA); } - final escrowIx = await EscrowInstruction.completeEscrow( - escrowAccount: aEscrow, - receiverTokenAccount: ataReceiver, - depositorAccount: platform.publicKey, - vaultTokenAccount: ataEscrow, + final iTransfer = TokenInstruction.transfer( + amount: escrow.amount, + source: ataEscrow, + destination: ataReceiver, + owner: aEscrow, + ); + + instructions.add(iTransfer); + + final iCloseAccount = TokenInstruction.closeAccount( + accountToClose: ataEscrow, + destination: platform.publicKey, + owner: aEscrow, ); - instructions.add(escrowIx); + instructions.add(iCloseAccount); final message = Message(instructions: instructions); final latestBlockhash = diff --git a/packages/espressocash_backend/pubspec.lock b/packages/espressocash_backend/pubspec.lock index 3d0ebe79fb..0ff42a9956 100644 --- a/packages/espressocash_backend/pubspec.lock +++ b/packages/espressocash_backend/pubspec.lock @@ -808,4 +808,4 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.2 <3.0.0" + dart: ">=2.19.0 <3.0.0" diff --git a/packages/espressocash_backend/share_escrow/.gitignore b/packages/espressocash_backend/share_escrow/.gitignore deleted file mode 100644 index 3cceda5578..0000000000 --- a/packages/espressocash_backend/share_escrow/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# https://dart.dev/guides/libraries/private-files -# Created by `dart pub` -.dart_tool/ - -# Avoid committing pubspec.lock for library packages; see -# https://dart.dev/guides/libraries/private-files#pubspeclock. -pubspec.lock diff --git a/packages/espressocash_backend/share_escrow/CHANGELOG.md b/packages/espressocash_backend/share_escrow/CHANGELOG.md deleted file mode 100644 index effe43c82c..0000000000 --- a/packages/espressocash_backend/share_escrow/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 1.0.0 - -- Initial version. diff --git a/packages/espressocash_backend/share_escrow/Makefile b/packages/espressocash_backend/share_escrow/Makefile deleted file mode 100644 index 041a142e28..0000000000 --- a/packages/espressocash_backend/share_escrow/Makefile +++ /dev/null @@ -1 +0,0 @@ -include ../../../Makefile diff --git a/packages/espressocash_backend/share_escrow/README.md b/packages/espressocash_backend/share_escrow/README.md deleted file mode 100644 index 8b55e735b5..0000000000 --- a/packages/espressocash_backend/share_escrow/README.md +++ /dev/null @@ -1,39 +0,0 @@ - - -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. - -## Features - -TODO: List what your package can do. Maybe include images, gifs, or videos. - -## Getting started - -TODO: List prerequisites and provide or point to information on how to -start using the package. - -## Usage - -TODO: Include short and useful examples for package users. Add longer examples -to `/example` folder. - -```dart -const like = 'sample'; -``` - -## Additional information - -TODO: Tell users more about the package: where to find more information, how to -contribute to the package, how to file issues, what response they can expect -from the package authors, and more. diff --git a/packages/espressocash_backend/share_escrow/analysis_options.yaml b/packages/espressocash_backend/share_escrow/analysis_options.yaml deleted file mode 100644 index 511c0ab2fe..0000000000 --- a/packages/espressocash_backend/share_escrow/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: package:mews_pedantic/analysis_options.yaml \ No newline at end of file diff --git a/packages/espressocash_backend/share_escrow/lib/share_escrow.dart b/packages/espressocash_backend/share_escrow/lib/share_escrow.dart deleted file mode 100644 index 95599b3cd4..0000000000 --- a/packages/espressocash_backend/share_escrow/lib/share_escrow.dart +++ /dev/null @@ -1,4 +0,0 @@ -library share_escrow; - -export 'src/instructions.dart'; -export 'src/models.dart'; diff --git a/packages/espressocash_backend/share_escrow/lib/src/instructions.dart b/packages/espressocash_backend/share_escrow/lib/src/instructions.dart deleted file mode 100644 index 3c18b177a5..0000000000 --- a/packages/espressocash_backend/share_escrow/lib/src/instructions.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:share_escrow/src/models.dart'; -import 'package:solana/anchor.dart'; -import 'package:solana/encoder.dart'; -import 'package:solana/solana.dart'; - -class EscrowInstruction { - static Future initEscrow({ - required int amount, - required Ed25519HDPublicKey escrowAccount, - required Ed25519HDPublicKey senderAccount, - required Ed25519HDPublicKey senderTokenAccount, - required Ed25519HDPublicKey depositorAccount, - required Ed25519HDPublicKey vaultTokenAccount, - }) => - AnchorInstruction.forMethod( - programId: escrowProgram, - method: 'initialize_escrow', - arguments: ByteArray( - EscrowArgument( - amount: BigInt.from(amount), - ).toBorsh().toList(), - ), - accounts: [ - AccountMeta.writeable(pubKey: escrowAccount, isSigner: true), - AccountMeta.writeable(pubKey: depositorAccount, isSigner: true), - AccountMeta.writeable(pubKey: senderAccount, isSigner: true), - AccountMeta.writeable(pubKey: senderTokenAccount, isSigner: false), - AccountMeta.writeable(pubKey: vaultTokenAccount, isSigner: false), - AccountMeta.readonly(pubKey: SystemProgram.id, isSigner: false), - AccountMeta.readonly(pubKey: TokenProgram.id, isSigner: false), - ], - namespace: 'global', - ); - - static Future completeEscrow({ - required Ed25519HDPublicKey escrowAccount, - required Ed25519HDPublicKey receiverTokenAccount, - required Ed25519HDPublicKey depositorAccount, - required Ed25519HDPublicKey vaultTokenAccount, - }) async => - AnchorInstruction.forMethod( - programId: escrowProgram, - method: 'complete_escrow', - accounts: [ - AccountMeta.writeable(pubKey: escrowAccount, isSigner: true), - AccountMeta.writeable(pubKey: depositorAccount, isSigner: true), - AccountMeta.writeable(pubKey: receiverTokenAccount, isSigner: false), - AccountMeta.writeable(pubKey: vaultTokenAccount, isSigner: false), - AccountMeta.readonly( - pubKey: await _calculatePda(escrowAccount), - isSigner: false, - ), - AccountMeta.readonly(pubKey: TokenProgram.id, isSigner: false), - ], - namespace: 'global', - ); - - static Future cancelEscrow({ - required Ed25519HDPublicKey escrowAccount, - required Ed25519HDPublicKey senderAccount, - required Ed25519HDPublicKey senderTokenAccount, - required Ed25519HDPublicKey depositorAccount, - required Ed25519HDPublicKey vaultTokenAccount, - }) async => - AnchorInstruction.forMethod( - programId: escrowProgram, - method: 'cancel_escrow', - accounts: [ - AccountMeta.writeable(pubKey: escrowAccount, isSigner: false), - AccountMeta.writeable(pubKey: senderAccount, isSigner: true), - AccountMeta.writeable(pubKey: depositorAccount, isSigner: true), - AccountMeta.writeable(pubKey: senderTokenAccount, isSigner: false), - AccountMeta.writeable(pubKey: vaultTokenAccount, isSigner: false), - AccountMeta.readonly( - pubKey: await _calculatePda(escrowAccount), - isSigner: false, - ), - AccountMeta.readonly(pubKey: TokenProgram.id, isSigner: false), - ], - namespace: 'global', - ); -} - -Future _calculatePda( - Ed25519HDPublicKey escrowAccount, -) async => - Ed25519HDPublicKey.findProgramAddress( - seeds: [ - 'ec_shareable_links'.codeUnits, - escrowAccount.bytes, - ], - programId: escrowProgram, - ); - -final escrowProgram = Ed25519HDPublicKey.fromBase58( - 'GHrMLBLnwGB8ypCWQnPeRzgHwePpUtSnY5ZSCCWzYmhC', -); diff --git a/packages/espressocash_backend/share_escrow/lib/src/models.dart b/packages/espressocash_backend/share_escrow/lib/src/models.dart deleted file mode 100644 index 47109b32b4..0000000000 --- a/packages/espressocash_backend/share_escrow/lib/src/models.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:borsh_annotation/borsh_annotation.dart'; -import 'package:solana/anchor.dart'; -import 'package:solana/dto.dart'; -import 'package:solana/solana.dart'; -// ignore: implementation_imports, Update when solana package is updated -import 'package:solana/src/borsh_ext.dart'; - -part 'models.g.dart'; - -class EscrowDataAccount implements AnchorAccount { - const EscrowDataAccount._({ - required this.discriminator, - required this.vaultTokenAccount, - required this.status, - required this.senderAccount, - required this.senderTokenAccount, - required this.depositorAccount, - }); - - factory EscrowDataAccount._fromBinary( - List bytes, - ) { - final accountData = - _EscrowDataAccount.fromBorsh(Uint8List.fromList(bytes.sublist(8))); - - return EscrowDataAccount._( - discriminator: bytes.sublist(0, 8), - senderAccount: accountData.senderAccount, - senderTokenAccount: accountData.senderTokenAccount, - depositorAccount: accountData.depositorAccount, - vaultTokenAccount: accountData.vaultTokenAccount, - status: EscrowStatus.values[accountData.status], - ); - } - - factory EscrowDataAccount.fromAccountData(AccountData? accountData) { - if (accountData is BinaryAccountData) { - return EscrowDataAccount._fromBinary(accountData.data); - } else { - throw const FormatException('invalid account data found'); - } - } - - @override - final List discriminator; - - final Ed25519HDPublicKey senderAccount; - final Ed25519HDPublicKey vaultTokenAccount; - final Ed25519HDPublicKey senderTokenAccount; - final Ed25519HDPublicKey depositorAccount; - final EscrowStatus status; -} - -@BorshSerializable() -class _EscrowDataAccount with _$_EscrowDataAccount { - factory _EscrowDataAccount({ - @BPublicKey() required Ed25519HDPublicKey senderAccount, - @BPublicKey() required Ed25519HDPublicKey vaultTokenAccount, - @BPublicKey() required Ed25519HDPublicKey senderTokenAccount, - @BPublicKey() required Ed25519HDPublicKey depositorAccount, - @BU8() required int status, - }) = __EscrowDataAccount; - - _EscrowDataAccount._(); - - factory _EscrowDataAccount.fromBorsh(Uint8List data) => - _$_EscrowDataAccountFromBorsh(data); -} - -enum EscrowStatus { - pending, - canceled, - completed; -} - -@BorshSerializable() -class EscrowArgument with _$EscrowArgument { - factory EscrowArgument({ - @BU64() required BigInt amount, - }) = _EscrowArgument; - - EscrowArgument._(); - - factory EscrowArgument.fromBorsh(Uint8List data) => - _$EscrowArgumentFromBorsh(data); -} diff --git a/packages/espressocash_backend/share_escrow/lib/src/models.g.dart b/packages/espressocash_backend/share_escrow/lib/src/models.g.dart deleted file mode 100644 index 8b0fd36823..0000000000 --- a/packages/espressocash_backend/share_escrow/lib/src/models.g.dart +++ /dev/null @@ -1,111 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'models.dart'; - -// ************************************************************************** -// BorshSerializableGenerator -// ************************************************************************** - -mixin _$_EscrowDataAccount { - Ed25519HDPublicKey get senderAccount => throw UnimplementedError(); - Ed25519HDPublicKey get vaultTokenAccount => throw UnimplementedError(); - Ed25519HDPublicKey get senderTokenAccount => throw UnimplementedError(); - Ed25519HDPublicKey get depositorAccount => throw UnimplementedError(); - int get status => throw UnimplementedError(); - - Uint8List toBorsh() { - final writer = BinaryWriter(); - - const BPublicKey().write(writer, senderAccount); - const BPublicKey().write(writer, vaultTokenAccount); - const BPublicKey().write(writer, senderTokenAccount); - const BPublicKey().write(writer, depositorAccount); - const BU8().write(writer, status); - - return writer.toArray(); - } -} - -class __EscrowDataAccount extends _EscrowDataAccount { - __EscrowDataAccount({ - required this.senderAccount, - required this.vaultTokenAccount, - required this.senderTokenAccount, - required this.depositorAccount, - required this.status, - }) : super._(); - - final Ed25519HDPublicKey senderAccount; - final Ed25519HDPublicKey vaultTokenAccount; - final Ed25519HDPublicKey senderTokenAccount; - final Ed25519HDPublicKey depositorAccount; - final int status; -} - -class B_EscrowDataAccount implements BType<_EscrowDataAccount> { - const B_EscrowDataAccount(); - - @override - void write(BinaryWriter writer, _EscrowDataAccount value) { - writer.writeStruct(value.toBorsh()); - } - - @override - _EscrowDataAccount read(BinaryReader reader) { - return _EscrowDataAccount( - senderAccount: const BPublicKey().read(reader), - vaultTokenAccount: const BPublicKey().read(reader), - senderTokenAccount: const BPublicKey().read(reader), - depositorAccount: const BPublicKey().read(reader), - status: const BU8().read(reader), - ); - } -} - -_EscrowDataAccount _$_EscrowDataAccountFromBorsh(Uint8List data) { - final reader = BinaryReader(data.buffer.asByteData()); - - return const B_EscrowDataAccount().read(reader); -} - -mixin _$EscrowArgument { - BigInt get amount => throw UnimplementedError(); - - Uint8List toBorsh() { - final writer = BinaryWriter(); - - const BU64().write(writer, amount); - - return writer.toArray(); - } -} - -class _EscrowArgument extends EscrowArgument { - _EscrowArgument({ - required this.amount, - }) : super._(); - - final BigInt amount; -} - -class BEscrowArgument implements BType { - const BEscrowArgument(); - - @override - void write(BinaryWriter writer, EscrowArgument value) { - writer.writeStruct(value.toBorsh()); - } - - @override - EscrowArgument read(BinaryReader reader) { - return EscrowArgument( - amount: const BU64().read(reader), - ); - } -} - -EscrowArgument _$EscrowArgumentFromBorsh(Uint8List data) { - final reader = BinaryReader(data.buffer.asByteData()); - - return const BEscrowArgument().read(reader); -} diff --git a/packages/espressocash_backend/share_escrow/pubspec.yaml b/packages/espressocash_backend/share_escrow/pubspec.yaml deleted file mode 100644 index 81cd31f3ec..0000000000 --- a/packages/espressocash_backend/share_escrow/pubspec.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: share_escrow -description: A starting point for Dart libraries or applications. -version: 1.0.0 -publish_to: "none" - -environment: - sdk: '>=2.19.2 <3.0.0' - -dependencies: - borsh: ^0.3.1+2 - borsh_annotation: ^0.3.1+3 - solana: ^0.29.0 - -dev_dependencies: - build_runner: ^2.3.2 - json_serializable: ^6.5.2 - mews_pedantic: ^0.13.0 - test: ^1.22.1 \ No newline at end of file diff --git a/packages/espressocash_backend/test/payments/cancel_payment_test.dart b/packages/espressocash_backend/test/payments/cancel_payment_test.dart deleted file mode 100644 index 663faf23ba..0000000000 --- a/packages/espressocash_backend/test/payments/cancel_payment_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:dfunc/dfunc.dart'; -import 'package:espressocash_backend/src/constants.dart'; -import 'package:espressocash_backend/src/payments/cancel_payment.dart'; -import 'package:espressocash_backend/src/payments/create_payment.dart'; -import 'package:solana/solana.dart'; -import 'package:test/test.dart'; - -import '../config.dart'; -import 'utils.dart'; - -void main() { - final client = createTestSolanaClient(); - - /// Initial token amount for the sender after creation. - const senderInitialAmount = 500000; - - const transferAmount = 300000; - - test('cancels payment', () async { - final testData = await createTestData( - client: client, - senderInitialAmount: senderInitialAmount, - ); - - final escrowAccount = await Ed25519HDKeyPair.random(); - - final result = await createPaymentTx( - aSender: testData.sender.publicKey, - aEscrow: escrowAccount.publicKey, - mint: testData.mint, - amount: transferAmount, - client: client, - platform: testData.platform, - commitment: Commitment.confirmed, - ); - - // Sender and Escrow has to resign the transaction with their private key. The tx is - // already partially signed by the platform. - final resignedTx = await result.item1 - .resign(testData.sender) - .letAsync((tx) => tx.resign(escrowAccount)); - - final signature = await client.rpcClient.sendTransaction( - resignedTx.encode(), - preflightCommitment: Commitment.confirmed, - ); - - await client.waitForSignatureStatus( - signature, - status: Commitment.confirmed, - ); - - final receiveResult = await cancelPaymentTx( - aEscrow: escrowAccount.publicKey, - aSender: testData.sender.publicKey, - mint: testData.mint, - platform: testData.platform, - client: client, - commitment: Commitment.confirmed, - ); - - final resignedReceiveTx = await receiveResult.item1.resign(testData.sender); - - final signatureReceive = await client.rpcClient.sendTransaction( - resignedReceiveTx.encode(), - preflightCommitment: Commitment.confirmed, - ); - - await client.waitForSignatureStatus( - signatureReceive, - status: Commitment.confirmed, - ); - - final senderBalance = await client.getMintBalance( - testData.sender.publicKey, - mint: testData.mint, - ); - - // Receiver should get the expected amount. - expect(senderBalance, senderInitialAmount - shareableLinkPaymentFee); - }); -} diff --git a/packages/espressocash_backend/test/payments/create_direct_payment_test.dart b/packages/espressocash_backend/test/payments/create_direct_payment_test.dart index 8a8d78466a..b666c4bf04 100644 --- a/packages/espressocash_backend/test/payments/create_direct_payment_test.dart +++ b/packages/espressocash_backend/test/payments/create_direct_payment_test.dart @@ -1,13 +1,12 @@ import 'package:espressocash_backend/src/constants.dart'; -import 'package:espressocash_backend/src/payments/create_direct_payment.dart'; +import 'package:espressocash_backend/src/payments/create_payment.dart'; import 'package:solana/solana.dart'; -import 'package:test/expect.dart'; -import 'package:test/scaffolding.dart'; +import 'package:test/test.dart'; import '../config.dart'; import 'utils.dart'; -void main() { +Future main() async { final client = createTestSolanaClient(); /// Initial token amount for the sender after creation. @@ -15,7 +14,11 @@ void main() { const transferAmount = 300000; - test('Receiver does not have ATA', () async { + /// Creates sender system account and ATA, minting [senderInitialAmount] to + /// the ATA. System account won't have any SOL, since sender is not paying any + /// fees in SOL. + + test('creates payment', () async { final data = await createTestData( client: client, senderInitialAmount: senderInitialAmount, @@ -24,24 +27,23 @@ void main() { final platform = data.platform; final mint = data.mint; - final receiver = await Ed25519HDKeyPair.random(); + // Sender creates an escrow account and keeps its private key. Sender + // doesn't transfer private key for the escrow account to the system. + final escrow = await Ed25519HDKeyPair.random(); - final payment = await createDirectPayment( + final result = await createPaymentTx( aSender: sender.publicKey, - aReceiver: receiver.publicKey, - aReference: null, + aEscrow: escrow.publicKey, mint: mint, amount: transferAmount, - platform: platform, client: client, + platform: platform, commitment: Commitment.confirmed, ); - expect(payment.fee, directPaymentWithAccountCreationFee); - // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await payment.transaction.resign(sender); + final resignedTx = await sender.resign(result.item1); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), @@ -55,19 +57,19 @@ void main() { expect( await client.getMintBalance(sender.publicKey, mint: mint), - senderInitialAmount - transferAmount - payment.fee, + senderInitialAmount - transferAmount - shareableLinkPaymentFee, ); expect( - await client.getMintBalance(receiver.publicKey, mint: mint), + await client.getMintBalance(escrow.publicKey, mint: mint), transferAmount, ); expect( await client.getMintBalance(platform.publicKey, mint: mint), - payment.fee, + shareableLinkPaymentFee, ); }); - test('Receiver has ATA', () async { + test('fails without resigning', () async { final data = await createTestData( client: client, senderInitialAmount: senderInitialAmount, @@ -76,53 +78,23 @@ void main() { final platform = data.platform; final mint = data.mint; - final receiver = await Ed25519HDKeyPair.random(); - - await client.createAssociatedTokenAccount( - mint: mint, - funder: data.mintAuthority, - owner: receiver.publicKey, - commitment: Commitment.confirmed, - ); + final escrow = await Ed25519HDKeyPair.random(); - final payment = await createDirectPayment( + final result = await createPaymentTx( aSender: sender.publicKey, - aReceiver: receiver.publicKey, - aReference: null, + aEscrow: escrow.publicKey, mint: mint, amount: transferAmount, - platform: platform, client: client, + platform: platform, commitment: Commitment.confirmed, ); - expect(payment.fee, directPaymentFee); - - // Sender has to resign the transaction with their private key. The tx is - // already partially signed by the platform. - final resignedTx = await payment.transaction.resign(sender); - - final signature = await client.rpcClient.sendTransaction( - resignedTx.encode(), + final signature = client.rpcClient.sendTransaction( + result.item1.encode(), preflightCommitment: Commitment.confirmed, ); - await client.waitForSignatureStatus( - signature, - status: Commitment.confirmed, - ); - - expect( - await client.getMintBalance(sender.publicKey, mint: mint), - senderInitialAmount - transferAmount - payment.fee, - ); - expect( - await client.getMintBalance(receiver.publicKey, mint: mint), - transferAmount, - ); - expect( - await client.getMintBalance(platform.publicKey, mint: mint), - payment.fee, - ); + expect(signature, throwsA(isA())); }); } diff --git a/packages/espressocash_backend/test/payments/create_payment_test.dart b/packages/espressocash_backend/test/payments/create_payment_test.dart index 17d1bd8531..b666c4bf04 100644 --- a/packages/espressocash_backend/test/payments/create_payment_test.dart +++ b/packages/espressocash_backend/test/payments/create_payment_test.dart @@ -1,4 +1,3 @@ -import 'package:dfunc/dfunc.dart'; import 'package:espressocash_backend/src/constants.dart'; import 'package:espressocash_backend/src/payments/create_payment.dart'; import 'package:solana/solana.dart'; @@ -42,10 +41,9 @@ Future main() async { commitment: Commitment.confirmed, ); - // Sender and Escrow has to resign the transaction with their private key. The tx is + // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = - await result.item1.resign(sender).letAsync((p) => p.resign(escrow)); + final resignedTx = await sender.resign(result.item1); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), diff --git a/packages/espressocash_backend/test/payments/escrow_account_test.dart b/packages/espressocash_backend/test/payments/escrow_account_test.dart index 6ec0169d7c..a3874f85df 100644 --- a/packages/espressocash_backend/test/payments/escrow_account_test.dart +++ b/packages/espressocash_backend/test/payments/escrow_account_test.dart @@ -1,4 +1,3 @@ -import 'package:dfunc/dfunc.dart'; import 'package:espressocash_backend/src/payments/create_payment.dart'; import 'package:espressocash_backend/src/payments/escrow_account.dart'; import 'package:solana/solana.dart'; @@ -77,11 +76,9 @@ void main() { commitment: Commitment.confirmed, ); - // Sender and Escrow has to resign the transaction with their private key. The tx is + // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await result.item1 - .resign(testData.sender) - .letAsync((tx) => tx.resign(escrowAccount)); + final resignedTx = await testData.sender.resign(result.item1); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), diff --git a/packages/espressocash_backend/test/payments/receive_payment_test.dart b/packages/espressocash_backend/test/payments/receive_payment_test.dart index 9bf15d327c..4202ee4d6e 100644 --- a/packages/espressocash_backend/test/payments/receive_payment_test.dart +++ b/packages/espressocash_backend/test/payments/receive_payment_test.dart @@ -1,4 +1,3 @@ -import 'package:dfunc/dfunc.dart'; import 'package:espressocash_backend/src/payments/create_payment.dart'; import 'package:espressocash_backend/src/payments/receive_payment.dart'; import 'package:solana/dto.dart'; @@ -37,9 +36,7 @@ void main() { // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await result.item1 - .resign(testData.sender) - .letAsync((tx) => tx.resign(escrowAccount)); + final resignedTx = await testData.sender.resign(result.item1); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), @@ -64,7 +61,7 @@ void main() { commitment: Commitment.confirmed, ); - final resignedReceiveTx = await receiveResult.item1.resign(escrowAccount); + final resignedReceiveTx = await escrowAccount.resign(receiveResult.item1); final signatureReceive = await client.rpcClient.sendTransaction( resignedReceiveTx.encode(), @@ -120,9 +117,7 @@ void main() { // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await createPaymentResult.item1 - .resign(testData.sender) - .letAsync((tx) => tx.resign(escrowAccount)); + final resignedTx = await testData.sender.resign(createPaymentResult.item1); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), @@ -147,7 +142,7 @@ void main() { commitment: Commitment.confirmed, ); - final resignedReceiveTx = await receiveResult.item1.resign(escrowAccount); + final resignedReceiveTx = await escrowAccount.resign(receiveResult.item1); final signatureReceive = await client.rpcClient.sendTransaction( resignedReceiveTx.encode(), diff --git a/packages/espressocash_backend/test/payments/utils.dart b/packages/espressocash_backend/test/payments/utils.dart index 714da9e0d6..c4e8f2d7ee 100644 --- a/packages/espressocash_backend/test/payments/utils.dart +++ b/packages/espressocash_backend/test/payments/utils.dart @@ -100,16 +100,15 @@ extension SolanaClientExt on SolanaClient { } } -extension SignedTxExt on SignedTx { - Future resign( - Ed25519HDKeyPair wallet, - ) async => - SignedTx( - signatures: signatures.toList() - ..setAll( - signatures.indexWhere((it) => it.publicKey == wallet.publicKey), [ - await wallet.sign(compiledMessage.toByteArray()), - ]), - compiledMessage: compiledMessage, - ); +extension Ed25519HDKeyPairExt on Ed25519HDKeyPair { + Future resign(SignedTx tx) async { + final compiledMessage = CompiledMessage(tx.compiledMessage.toByteArray()); + + return SignedTx( + signatures: tx.signatures.toList() + ..removeLast() + ..add(await sign(compiledMessage.toByteArray())), + compiledMessage: compiledMessage, + ); + } } diff --git a/packages/espressocash_backend/test/share_escrow/escrow_test.dart b/packages/espressocash_backend/test/share_escrow/escrow_test.dart deleted file mode 100644 index 3071df6435..0000000000 --- a/packages/espressocash_backend/test/share_escrow/escrow_test.dart +++ /dev/null @@ -1,211 +0,0 @@ -import 'package:share_escrow/src/instructions.dart'; -import 'package:share_escrow/src/models.dart'; -import 'package:solana/dto.dart'; -import 'package:solana/solana.dart'; -import 'package:test/test.dart'; - -import '../config.dart'; -import 'utils.dart'; - -void main() { - final client = createTestSolanaClient(); - - late Mint mint; - late Ed25519HDKeyPair mintAuthority; - - const initializerAmount = 1000; - const sendAmount = 700; - - const airdropAmount = 1000000000; - - Future requestAirdrop(Ed25519HDKeyPair account) async { - await client.requestAirdrop( - address: account.publicKey, - lamports: airdropAmount, - commitment: Commitment.confirmed, - ); - } - - setUpAll(() async { - mintAuthority = await Ed25519HDKeyPair.random(); - - await requestAirdrop(mintAuthority); - - mint = await client.initializeMint( - mintAuthority: mintAuthority, - decimals: 5, - commitment: Commitment.confirmed, - ); - }); - - Future fetchEscrowAccount( - Ed25519HDPublicKey escrowAccount, - ) async { - final account = await client.rpcClient.getAccountInfo( - escrowAccount.toBase58(), - encoding: Encoding.base64, - commitment: Commitment.confirmed, - ); - - if (account.value == null) { - fail('Escrow account is null'); - } - - return EscrowDataAccount.fromAccountData(account.value?.data); - } - - Future createEscrow(Accounts accounts) async { - final instruction = await EscrowInstruction.initEscrow( - amount: sendAmount, - escrowAccount: accounts.escrowAccount.publicKey, - senderAccount: accounts.senderAccount.publicKey, - depositorAccount: accounts.depositorAccount.publicKey, - senderTokenAccount: accounts.senderTokenAccount, - vaultTokenAccount: accounts.vaultTokenAccount, - ); - - await client.sendAndConfirmTransaction( - message: Message.only(instruction), - signers: [ - accounts.depositorAccount, - accounts.escrowAccount, - accounts.senderAccount, - ], - commitment: Commitment.confirmed, - ); - } - - Future createAccounts() async { - final Ed25519HDKeyPair escrow = await Ed25519HDKeyPair.random(); - final Ed25519HDKeyPair sender = await Ed25519HDKeyPair.random(); - final Ed25519HDKeyPair receiver = await Ed25519HDKeyPair.random(); - final Ed25519HDKeyPair depositor = await Ed25519HDKeyPair.random(); - - await requestAirdrop(sender); - await requestAirdrop(receiver); - await requestAirdrop(depositor); - - final sellerTokenAccount = - await createAccount(client: client, owner: sender, mint: mint); - - await client.mintTo( - mint: mint.address, - destination: sellerTokenAccount, - amount: initializerAmount, - authority: mintAuthority, - commitment: Commitment.confirmed, - ); - - final vault = await client.createAssociatedTokenAccount( - owner: escrow.publicKey, - mint: mint.address, - funder: depositor, - commitment: Commitment.confirmed, - ); - - return Accounts( - escrowAccount: escrow, - senderAccount: sender, - receiverAccount: receiver, - depositorAccount: depositor, - senderTokenAccount: sellerTokenAccount, - receiverTokenAccount: - await createAccount(client: client, owner: receiver, mint: mint), - vaultTokenAccount: Ed25519HDPublicKey.fromBase58(vault.pubkey), - ); - } - - test('Initialize Escrow', () async { - final accounts = await createAccounts(); - - await createEscrow(accounts); - - final escrowAccount = - await fetchEscrowAccount(accounts.escrowAccount.publicKey); - - expect(EscrowStatus.pending, escrowAccount.status); - - final senderTokenAccount = await getTokenAccountBalance( - client: client, - account: accounts.senderTokenAccount, - ); - - if (senderTokenAccount == null) { - fail('Sender token account is null'); - } - - expect( - initializerAmount - sendAmount, - int.tryParse(senderTokenAccount.amount) ?? 0, - ); - - final depositorTokenAccount = await getTokenAccountBalance( - client: client, - account: accounts.vaultTokenAccount, - ); - - if (depositorTokenAccount == null) { - fail('Depositor token account is null'); - } - - expect( - sendAmount, - int.tryParse(depositorTokenAccount.amount) ?? 0, - ); - }); - - test('Cancel Escrow', () async { - final accounts = await createAccounts(); - - await createEscrow(accounts); - - final instruction = await EscrowInstruction.cancelEscrow( - escrowAccount: accounts.escrowAccount.publicKey, - senderAccount: accounts.senderAccount.publicKey, - depositorAccount: accounts.depositorAccount.publicKey, - senderTokenAccount: accounts.senderTokenAccount, - vaultTokenAccount: accounts.vaultTokenAccount, - ); - - await client.sendAndConfirmTransaction( - message: Message.only(instruction), - signers: [ - accounts.depositorAccount, - accounts.senderAccount, - ], - commitment: Commitment.confirmed, - ); - - final escrowAccount = - await fetchEscrowAccount(accounts.escrowAccount.publicKey); - - expect(EscrowStatus.canceled, escrowAccount.status); - }); - - test('Complete Escrow', () async { - final accounts = await createAccounts(); - - await createEscrow(accounts); - - final instruction = await EscrowInstruction.completeEscrow( - escrowAccount: accounts.escrowAccount.publicKey, - receiverTokenAccount: accounts.receiverTokenAccount, - depositorAccount: accounts.depositorAccount.publicKey, - vaultTokenAccount: accounts.vaultTokenAccount, - ); - - await client.sendAndConfirmTransaction( - message: Message.only(instruction), - signers: [ - accounts.depositorAccount, - accounts.escrowAccount, - ], - commitment: Commitment.confirmed, - ); - - final escrowAccount = - await fetchEscrowAccount(accounts.escrowAccount.publicKey); - - expect(EscrowStatus.completed, escrowAccount.status); - }); -} diff --git a/packages/espressocash_backend/test/share_escrow/utils.dart b/packages/espressocash_backend/test/share_escrow/utils.dart deleted file mode 100644 index e550c43125..0000000000 --- a/packages/espressocash_backend/test/share_escrow/utils.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:solana/dto.dart'; -import 'package:solana/solana.dart'; - -class Accounts { - Accounts({ - required this.escrowAccount, - required this.receiverAccount, - required this.senderTokenAccount, - required this.vaultTokenAccount, - required this.senderAccount, - required this.depositorAccount, - required this.receiverTokenAccount, - }); - final Ed25519HDKeyPair escrowAccount; - final Ed25519HDKeyPair receiverAccount; - final Ed25519HDKeyPair senderAccount; - final Ed25519HDKeyPair depositorAccount; - final Ed25519HDPublicKey senderTokenAccount; - final Ed25519HDPublicKey receiverTokenAccount; - final Ed25519HDPublicKey vaultTokenAccount; -} - -Future createAccount({ - required SolanaClient client, - required Ed25519HDKeyPair owner, - required Mint mint, -}) async { - final accountKeyPair = await Ed25519HDKeyPair.random(); - - await client.createTokenAccount( - mint: mint.address, - account: accountKeyPair, - creator: owner, - commitment: Commitment.confirmed, - ); - - return accountKeyPair.publicKey; -} - -Future getTokenAccountBalance({ - required SolanaClient client, - required Ed25519HDPublicKey account, -}) async { - final info = await client.rpcClient.getAccountInfo( - account.toBase58(), - encoding: Encoding.jsonParsed, - commitment: Commitment.confirmed, - ); - - final accountData = info.value?.data; - - if (accountData is ParsedAccountData) { - return accountData.maybeWhen( - splToken: (parsed) => parsed.maybeMap( - account: (a) => a.info.tokenAmount, - orElse: () => null, - ), - orElse: () => null, - ); - } - - return null; -} - -Future getTokenTest({ - required SolanaClient client, - required Ed25519HDPublicKey account, - required Ed25519HDPublicKey mint, -}) async => - client.getTokenBalance(owner: account, mint: mint); From 0638407970aa03e45258112233dbadfa8f2d009d Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Mon, 24 Apr 2023 19:20:31 +0800 Subject: [PATCH 16/23] revert --- .../lib/src/dto/create_payment.freezed.dart | 343 ------------------ .../lib/src/dto/create_payment.g.dart | 30 -- .../payments/create_direct_payment_test.dart | 80 ++-- 3 files changed, 54 insertions(+), 399 deletions(-) diff --git a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.freezed.dart b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.freezed.dart index 3b2517549a..5532157427 100644 --- a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.freezed.dart +++ b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.freezed.dart @@ -727,349 +727,6 @@ abstract class _ReceivePaymentResponseDto implements ReceivePaymentResponseDto { get copyWith => throw _privateConstructorUsedError; } -CancelPaymentRequestDto _$CancelPaymentRequestDtoFromJson( - Map json) { - return _CancelPaymentRequestDto.fromJson(json); -} - -/// @nodoc -mixin _$CancelPaymentRequestDto { - String get senderAccount => throw _privateConstructorUsedError; - String get escrowAccount => throw _privateConstructorUsedError; - Cluster get cluster => throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $CancelPaymentRequestDtoCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $CancelPaymentRequestDtoCopyWith<$Res> { - factory $CancelPaymentRequestDtoCopyWith(CancelPaymentRequestDto value, - $Res Function(CancelPaymentRequestDto) then) = - _$CancelPaymentRequestDtoCopyWithImpl<$Res, CancelPaymentRequestDto>; - @useResult - $Res call({String senderAccount, String escrowAccount, Cluster cluster}); -} - -/// @nodoc -class _$CancelPaymentRequestDtoCopyWithImpl<$Res, - $Val extends CancelPaymentRequestDto> - implements $CancelPaymentRequestDtoCopyWith<$Res> { - _$CancelPaymentRequestDtoCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? senderAccount = null, - Object? escrowAccount = null, - Object? cluster = null, - }) { - return _then(_value.copyWith( - senderAccount: null == senderAccount - ? _value.senderAccount - : senderAccount // ignore: cast_nullable_to_non_nullable - as String, - escrowAccount: null == escrowAccount - ? _value.escrowAccount - : escrowAccount // ignore: cast_nullable_to_non_nullable - as String, - cluster: null == cluster - ? _value.cluster - : cluster // ignore: cast_nullable_to_non_nullable - as Cluster, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$_CancelPaymentRequestDtoCopyWith<$Res> - implements $CancelPaymentRequestDtoCopyWith<$Res> { - factory _$$_CancelPaymentRequestDtoCopyWith(_$_CancelPaymentRequestDto value, - $Res Function(_$_CancelPaymentRequestDto) then) = - __$$_CancelPaymentRequestDtoCopyWithImpl<$Res>; - @override - @useResult - $Res call({String senderAccount, String escrowAccount, Cluster cluster}); -} - -/// @nodoc -class __$$_CancelPaymentRequestDtoCopyWithImpl<$Res> - extends _$CancelPaymentRequestDtoCopyWithImpl<$Res, - _$_CancelPaymentRequestDto> - implements _$$_CancelPaymentRequestDtoCopyWith<$Res> { - __$$_CancelPaymentRequestDtoCopyWithImpl(_$_CancelPaymentRequestDto _value, - $Res Function(_$_CancelPaymentRequestDto) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? senderAccount = null, - Object? escrowAccount = null, - Object? cluster = null, - }) { - return _then(_$_CancelPaymentRequestDto( - senderAccount: null == senderAccount - ? _value.senderAccount - : senderAccount // ignore: cast_nullable_to_non_nullable - as String, - escrowAccount: null == escrowAccount - ? _value.escrowAccount - : escrowAccount // ignore: cast_nullable_to_non_nullable - as String, - cluster: null == cluster - ? _value.cluster - : cluster // ignore: cast_nullable_to_non_nullable - as Cluster, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$_CancelPaymentRequestDto implements _CancelPaymentRequestDto { - const _$_CancelPaymentRequestDto( - {required this.senderAccount, - required this.escrowAccount, - required this.cluster}); - - factory _$_CancelPaymentRequestDto.fromJson(Map json) => - _$$_CancelPaymentRequestDtoFromJson(json); - - @override - final String senderAccount; - @override - final String escrowAccount; - @override - final Cluster cluster; - - @override - String toString() { - return 'CancelPaymentRequestDto(senderAccount: $senderAccount, escrowAccount: $escrowAccount, cluster: $cluster)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$_CancelPaymentRequestDto && - (identical(other.senderAccount, senderAccount) || - other.senderAccount == senderAccount) && - (identical(other.escrowAccount, escrowAccount) || - other.escrowAccount == escrowAccount) && - (identical(other.cluster, cluster) || other.cluster == cluster)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => - Object.hash(runtimeType, senderAccount, escrowAccount, cluster); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$_CancelPaymentRequestDtoCopyWith<_$_CancelPaymentRequestDto> - get copyWith => - __$$_CancelPaymentRequestDtoCopyWithImpl<_$_CancelPaymentRequestDto>( - this, _$identity); - - @override - Map toJson() { - return _$$_CancelPaymentRequestDtoToJson( - this, - ); - } -} - -abstract class _CancelPaymentRequestDto implements CancelPaymentRequestDto { - const factory _CancelPaymentRequestDto( - {required final String senderAccount, - required final String escrowAccount, - required final Cluster cluster}) = _$_CancelPaymentRequestDto; - - factory _CancelPaymentRequestDto.fromJson(Map json) = - _$_CancelPaymentRequestDto.fromJson; - - @override - String get senderAccount; - @override - String get escrowAccount; - @override - Cluster get cluster; - @override - @JsonKey(ignore: true) - _$$_CancelPaymentRequestDtoCopyWith<_$_CancelPaymentRequestDto> - get copyWith => throw _privateConstructorUsedError; -} - -CancelPaymentResponseDto _$CancelPaymentResponseDtoFromJson( - Map json) { - return _CancelPaymentResponseDto.fromJson(json); -} - -/// @nodoc -mixin _$CancelPaymentResponseDto { - String get transaction => throw _privateConstructorUsedError; - BigInt get slot => throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $CancelPaymentResponseDtoCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $CancelPaymentResponseDtoCopyWith<$Res> { - factory $CancelPaymentResponseDtoCopyWith(CancelPaymentResponseDto value, - $Res Function(CancelPaymentResponseDto) then) = - _$CancelPaymentResponseDtoCopyWithImpl<$Res, CancelPaymentResponseDto>; - @useResult - $Res call({String transaction, BigInt slot}); -} - -/// @nodoc -class _$CancelPaymentResponseDtoCopyWithImpl<$Res, - $Val extends CancelPaymentResponseDto> - implements $CancelPaymentResponseDtoCopyWith<$Res> { - _$CancelPaymentResponseDtoCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? transaction = null, - Object? slot = null, - }) { - return _then(_value.copyWith( - transaction: null == transaction - ? _value.transaction - : transaction // ignore: cast_nullable_to_non_nullable - as String, - slot: null == slot - ? _value.slot - : slot // ignore: cast_nullable_to_non_nullable - as BigInt, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$_CancelPaymentResponseDtoCopyWith<$Res> - implements $CancelPaymentResponseDtoCopyWith<$Res> { - factory _$$_CancelPaymentResponseDtoCopyWith( - _$_CancelPaymentResponseDto value, - $Res Function(_$_CancelPaymentResponseDto) then) = - __$$_CancelPaymentResponseDtoCopyWithImpl<$Res>; - @override - @useResult - $Res call({String transaction, BigInt slot}); -} - -/// @nodoc -class __$$_CancelPaymentResponseDtoCopyWithImpl<$Res> - extends _$CancelPaymentResponseDtoCopyWithImpl<$Res, - _$_CancelPaymentResponseDto> - implements _$$_CancelPaymentResponseDtoCopyWith<$Res> { - __$$_CancelPaymentResponseDtoCopyWithImpl(_$_CancelPaymentResponseDto _value, - $Res Function(_$_CancelPaymentResponseDto) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? transaction = null, - Object? slot = null, - }) { - return _then(_$_CancelPaymentResponseDto( - transaction: null == transaction - ? _value.transaction - : transaction // ignore: cast_nullable_to_non_nullable - as String, - slot: null == slot - ? _value.slot - : slot // ignore: cast_nullable_to_non_nullable - as BigInt, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$_CancelPaymentResponseDto implements _CancelPaymentResponseDto { - const _$_CancelPaymentResponseDto( - {required this.transaction, required this.slot}); - - factory _$_CancelPaymentResponseDto.fromJson(Map json) => - _$$_CancelPaymentResponseDtoFromJson(json); - - @override - final String transaction; - @override - final BigInt slot; - - @override - String toString() { - return 'CancelPaymentResponseDto(transaction: $transaction, slot: $slot)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$_CancelPaymentResponseDto && - (identical(other.transaction, transaction) || - other.transaction == transaction) && - (identical(other.slot, slot) || other.slot == slot)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash(runtimeType, transaction, slot); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$_CancelPaymentResponseDtoCopyWith<_$_CancelPaymentResponseDto> - get copyWith => __$$_CancelPaymentResponseDtoCopyWithImpl< - _$_CancelPaymentResponseDto>(this, _$identity); - - @override - Map toJson() { - return _$$_CancelPaymentResponseDtoToJson( - this, - ); - } -} - -abstract class _CancelPaymentResponseDto implements CancelPaymentResponseDto { - const factory _CancelPaymentResponseDto( - {required final String transaction, - required final BigInt slot}) = _$_CancelPaymentResponseDto; - - factory _CancelPaymentResponseDto.fromJson(Map json) = - _$_CancelPaymentResponseDto.fromJson; - - @override - String get transaction; - @override - BigInt get slot; - @override - @JsonKey(ignore: true) - _$$_CancelPaymentResponseDtoCopyWith<_$_CancelPaymentResponseDto> - get copyWith => throw _privateConstructorUsedError; -} - CreateDirectPaymentRequestDto _$CreateDirectPaymentRequestDtoFromJson( Map json) { return _CreateDirectPaymentRequestDto.fromJson(json); diff --git a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.g.dart b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.g.dart index ff62f1cfd2..183e158394 100644 --- a/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.g.dart +++ b/packages/espressocash_backend/espressocash_api/lib/src/dto/create_payment.g.dart @@ -73,36 +73,6 @@ Map _$$_ReceivePaymentResponseDtoToJson( 'slot': instance.slot.toString(), }; -_$_CancelPaymentRequestDto _$$_CancelPaymentRequestDtoFromJson( - Map json) => - _$_CancelPaymentRequestDto( - senderAccount: json['senderAccount'] as String, - escrowAccount: json['escrowAccount'] as String, - cluster: $enumDecode(_$ClusterEnumMap, json['cluster']), - ); - -Map _$$_CancelPaymentRequestDtoToJson( - _$_CancelPaymentRequestDto instance) => - { - 'senderAccount': instance.senderAccount, - 'escrowAccount': instance.escrowAccount, - 'cluster': _$ClusterEnumMap[instance.cluster]!, - }; - -_$_CancelPaymentResponseDto _$$_CancelPaymentResponseDtoFromJson( - Map json) => - _$_CancelPaymentResponseDto( - transaction: json['transaction'] as String, - slot: BigInt.parse(json['slot'] as String), - ); - -Map _$$_CancelPaymentResponseDtoToJson( - _$_CancelPaymentResponseDto instance) => - { - 'transaction': instance.transaction, - 'slot': instance.slot.toString(), - }; - _$_CreateDirectPaymentRequestDto _$$_CreateDirectPaymentRequestDtoFromJson( Map json) => _$_CreateDirectPaymentRequestDto( diff --git a/packages/espressocash_backend/test/payments/create_direct_payment_test.dart b/packages/espressocash_backend/test/payments/create_direct_payment_test.dart index b666c4bf04..7b80e4101c 100644 --- a/packages/espressocash_backend/test/payments/create_direct_payment_test.dart +++ b/packages/espressocash_backend/test/payments/create_direct_payment_test.dart @@ -1,12 +1,13 @@ import 'package:espressocash_backend/src/constants.dart'; -import 'package:espressocash_backend/src/payments/create_payment.dart'; +import 'package:espressocash_backend/src/payments/create_direct_payment.dart'; import 'package:solana/solana.dart'; -import 'package:test/test.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; import '../config.dart'; import 'utils.dart'; -Future main() async { +void main() { final client = createTestSolanaClient(); /// Initial token amount for the sender after creation. @@ -14,11 +15,7 @@ Future main() async { const transferAmount = 300000; - /// Creates sender system account and ATA, minting [senderInitialAmount] to - /// the ATA. System account won't have any SOL, since sender is not paying any - /// fees in SOL. - - test('creates payment', () async { + test('Receiver does not have ATA', () async { final data = await createTestData( client: client, senderInitialAmount: senderInitialAmount, @@ -27,23 +24,24 @@ Future main() async { final platform = data.platform; final mint = data.mint; - // Sender creates an escrow account and keeps its private key. Sender - // doesn't transfer private key for the escrow account to the system. - final escrow = await Ed25519HDKeyPair.random(); + final receiver = await Ed25519HDKeyPair.random(); - final result = await createPaymentTx( + final payment = await createDirectPayment( aSender: sender.publicKey, - aEscrow: escrow.publicKey, + aReceiver: receiver.publicKey, + aReference: null, mint: mint, amount: transferAmount, - client: client, platform: platform, + client: client, commitment: Commitment.confirmed, ); + expect(payment.fee, directPaymentWithAccountCreationFee); + // Sender has to resign the transaction with their private key. The tx is // already partially signed by the platform. - final resignedTx = await sender.resign(result.item1); + final resignedTx = await sender.resign(payment.transaction); final signature = await client.rpcClient.sendTransaction( resignedTx.encode(), @@ -57,19 +55,19 @@ Future main() async { expect( await client.getMintBalance(sender.publicKey, mint: mint), - senderInitialAmount - transferAmount - shareableLinkPaymentFee, + senderInitialAmount - transferAmount - payment.fee, ); expect( - await client.getMintBalance(escrow.publicKey, mint: mint), + await client.getMintBalance(receiver.publicKey, mint: mint), transferAmount, ); expect( await client.getMintBalance(platform.publicKey, mint: mint), - shareableLinkPaymentFee, + payment.fee, ); }); - test('fails without resigning', () async { + test('Receiver has ATA', () async { final data = await createTestData( client: client, senderInitialAmount: senderInitialAmount, @@ -78,23 +76,53 @@ Future main() async { final platform = data.platform; final mint = data.mint; - final escrow = await Ed25519HDKeyPair.random(); + final receiver = await Ed25519HDKeyPair.random(); + + await client.createAssociatedTokenAccount( + mint: mint, + funder: data.mintAuthority, + owner: receiver.publicKey, + commitment: Commitment.confirmed, + ); - final result = await createPaymentTx( + final payment = await createDirectPayment( aSender: sender.publicKey, - aEscrow: escrow.publicKey, + aReceiver: receiver.publicKey, + aReference: null, mint: mint, amount: transferAmount, - client: client, platform: platform, + client: client, commitment: Commitment.confirmed, ); - final signature = client.rpcClient.sendTransaction( - result.item1.encode(), + expect(payment.fee, directPaymentFee); + + // Sender has to resign the transaction with their private key. The tx is + // already partially signed by the platform. + final resignedTx = await sender.resign(payment.transaction); + + final signature = await client.rpcClient.sendTransaction( + resignedTx.encode(), preflightCommitment: Commitment.confirmed, ); - expect(signature, throwsA(isA())); + await client.waitForSignatureStatus( + signature, + status: Commitment.confirmed, + ); + + expect( + await client.getMintBalance(sender.publicKey, mint: mint), + senderInitialAmount - transferAmount - payment.fee, + ); + expect( + await client.getMintBalance(receiver.publicKey, mint: mint), + transferAmount, + ); + expect( + await client.getMintBalance(platform.publicKey, mint: mint), + payment.fee, + ); }); } From 8c7496c6dd7bc6d7852e1dff4876f3629c468653 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Mon, 24 Apr 2023 19:39:57 +0800 Subject: [PATCH 17/23] upd --- .../src/bl/recover_pending_watcher.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart index be88385fa6..046825020a 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart @@ -7,6 +7,7 @@ import 'package:solana/solana.dart'; import 'package:uuid/uuid.dart'; import '../../../../core/amount.dart'; +import '../../../../core/api_version.dart'; import '../../../../core/currency.dart'; import '../../../../core/escrow_private_key.dart'; import '../../../../core/tokens/token.dart'; @@ -99,6 +100,7 @@ class RecoverPendingWatcher { status: OSKPStatus.recovered(escrow: escrow), created: timestamp, linksGeneratedAt: timestamp, + apiVersion: SplitKeyApiVersion.smartContract, ), ); } From e78aaf7d1a23fbd306523beb4022a071b9d8e54e Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Mon, 24 Apr 2023 19:54:11 +0800 Subject: [PATCH 18/23] upd address --- packages/espressocash_app/lib/config.dart | 3 +++ .../src/bl/recover_pending_watcher.dart | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/espressocash_app/lib/config.dart b/packages/espressocash_app/lib/config.dart index 29c5201850..143cd6ef1a 100644 --- a/packages/espressocash_app/lib/config.dart +++ b/packages/espressocash_app/lib/config.dart @@ -79,3 +79,6 @@ const intercomAndroidKey = String.fromEnvironment('INTERCOM_ANDROID_KEY'); const rampApiKey = String.fromEnvironment('RAMP_API_KEY'); const maxPayloadsPerSigningRequest = 10; + +// Escrow payment address +const escrowScAddress = '7rE2We9zMQzj2xmhJRTvYXKP22VKDGh3krujdBqWibBL'; diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart index 046825020a..60a0817190 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart @@ -6,6 +6,7 @@ import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; import 'package:uuid/uuid.dart'; +import '../../../../config.dart'; import '../../../../core/amount.dart'; import '../../../../core/api_version.dart'; import '../../../../core/currency.dart'; @@ -45,9 +46,7 @@ class RecoverPendingWatcher { // Check if the transaction has interacted with the escrow smart contract final accounts = tx.compiledMessage.accountKeys; final hasInteractedWithEscrow = accounts.contains( - Ed25519HDPublicKey.fromBase58( - 'GHrMLBLnwGB8ypCWQnPeRzgHwePpUtSnY5ZSCCWzYmhC', - ), + Ed25519HDPublicKey.fromBase58(escrowScAddress), ); if (hasInteractedWithEscrow) { From 372638ec8887cf3f8501091e11fb780b34bfe27c Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Mon, 24 Apr 2023 19:56:08 +0800 Subject: [PATCH 19/23] dump schema --- packages/espressocash_app/moor_schemas/moor_schema_v37.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/espressocash_app/moor_schemas/moor_schema_v37.json diff --git a/packages/espressocash_app/moor_schemas/moor_schema_v37.json b/packages/espressocash_app/moor_schemas/moor_schema_v37.json new file mode 100644 index 0000000000..589ecaf3aa --- /dev/null +++ b/packages/espressocash_app/moor_schemas/moor_schema_v37.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.0.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"outgoing_transfer_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":1,"references":[],"type":"table","data":{"name":"payment_request_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"payer_name","getter_name":"payerName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"dynamic_link","getter_name":"dynamicLink","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(PaymentRequestStateDto.values)","dart_type_name":"PaymentRequestStateDto"}},{"name":"transaction_id","getter_name":"transactionId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"recipient","getter_name":"recipient","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"amount","getter_name":"amount","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"splt_token","getter_name":"spltToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"reference","getter_name":"reference","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"label","getter_name":"label","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"message","getter_name":"message","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"memo","getter_name":"memo","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"o_d_p_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"receiver","getter_name":"receiver","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"reference","getter_name":"reference","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ODPStatusDto.values)","dart_type_name":"ODPStatusDto"}},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"o_s_k_p_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(OSKPStatusDto.values)","dart_type_name":"OSKPStatusDto"}},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"withdraw_tx_id","getter_name":"withdrawTxId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"link1","getter_name":"link1","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"link2","getter_name":"link2","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"link3","getter_name":"link3","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"cancel_tx","getter_name":"cancelTx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"cancel_tx_id","getter_name":"cancelTxId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"generated_links_at","getter_name":"generatedLinksAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"resolved_at","getter_name":"resolvedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"api_version","getter_name":"apiVersion","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(OskpApiVersionDto.values)","dart_type_name":"OskpApiVersionDto"}},{"name":"public_key","getter_name":"publicKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":4,"references":[],"type":"table","data":{"name":"i_s_k_p_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ISKPStatusDto.values)","dart_type_name":"ISKPStatusDto"}},{"name":"api_version","getter_name":"apiVersion","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(IskpApiVersionDto.values)","dart_type_name":"IskpApiVersionDto"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":5,"references":[],"type":"table","data":{"name":"swap_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(SwapStatusDto.values)","dart_type_name":"SwapStatusDto"}},{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"input_mint","getter_name":"inputMint","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"output_mint","getter_name":"outputMint","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"slippage","getter_name":"slippage","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(SlippageDto.values)","dart_type_name":"SlippageDto"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":6,"references":[],"type":"table","data":{"name":"transaction_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"encoded_tx","getter_name":"encodedTx","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxCommonStatus.values)","dart_type_name":"TxCommonStatus"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":7,"references":[],"type":"table","data":{"name":"favorite_token_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"symbol","getter_name":"symbol","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"logo_uri","getter_name":"logoUri","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":8,"references":[],"type":"table","data":{"name":"popular_token_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"symbol","getter_name":"symbol","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"logo_uri","getter_name":"logoUri","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"price","getter_name":"price","moor_type":"double","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":9,"references":[],"type":"table","data":{"name":"o_t_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(OTStatusDto.values)","dart_type_name":"OTStatusDto"}},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"withdraw_tx_id","getter_name":"withdrawTxId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"link","getter_name":"link","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"cancel_tx","getter_name":"cancelTx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"cancel_tx_id","getter_name":"cancelTxId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":10,"references":[],"type":"table","data":{"name":"i_t_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ITStatusDto.values)","dart_type_name":"ITStatusDto"}},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}}]} \ No newline at end of file From 29aad7275f498c31bd9d87cd34c39d4b06b8eda5 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Mon, 24 Apr 2023 20:15:37 +0800 Subject: [PATCH 20/23] fix error --- packages/espressocash_app/lib/data/db/db.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/espressocash_app/lib/data/db/db.dart b/packages/espressocash_app/lib/data/db/db.dart index 06e4aa6583..582743bbb0 100644 --- a/packages/espressocash_app/lib/data/db/db.dart +++ b/packages/espressocash_app/lib/data/db/db.dart @@ -149,7 +149,7 @@ class MyDatabase extends _$MyDatabase { if (from < 36) { await m.deleteTable('i_s_l_p_rows'); } - if (from >= 16 && from < 35) { + if (from >= 16 && from < 37) { await m.addColumn(oSKPRows, oSKPRows.publicKey); } }, From 7eddd669687b9ed9a86dce0bd422510f7347d118 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Mon, 24 Apr 2023 20:44:43 +0800 Subject: [PATCH 21/23] upd --- .../src/bl/repository.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart index 89e5595a50..d7b8561812 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart @@ -197,7 +197,7 @@ extension on OSKPStatusDto { final txId = row.txId; final withdrawTxId = row.withdrawTxId; final escrow = row.privateKey?.let(base58decode).let(EscrowPrivateKey.new); - final escrowPk = await escrow?.keyPair.then((e) => e.publicKey); + final escrowPubkey = await escrow?.keyPair.then((e) => e.publicKey); final link1 = row.link1?.let(Uri.parse); final link2 = row.link2?.let(Uri.parse); final link3 = row.link3?.let(Uri.tryParse); @@ -264,30 +264,30 @@ extension on OSKPStatusDto { case OSKPStatusDto.cancelTxCreated: return OSKPStatus.cancelTxCreated( cancelTx!, - escrow: escrowPk!, + escrow: escrowPubkey!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.cancelTxFailure: return OSKPStatus.cancelTxFailure( - escrow: escrowPk!, + escrow: escrowPubkey!, reason: row.txFailureReason ?? TxFailureReason.unknown, ); case OSKPStatusDto.cancelTxSent: return OSKPStatus.cancelTxSent( cancelTx!, - escrow: escrowPk!, + escrow: escrowPubkey!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.cancelTxSendFailure: return OSKPStatus.cancelTxCreated( cancelTx!, - escrow: escrowPk!, + escrow: escrowPubkey!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.cancelTxWaitFailure: return OSKPStatus.cancelTxSent( cancelTx!, - escrow: escrowPk!, + escrow: escrowPubkey!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.recovered: From b5c603e29c76e9409e2b1bd8e4be0773793236ea Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Tue, 2 May 2023 20:01:20 +0800 Subject: [PATCH 22/23] upd --- .../models/outgoing_split_key_payment.dart | 30 +++++++- .../outgoing_split_key_payments/module.dart | 14 ++++ .../src/bl/oskp_service.dart | 61 +++++++++++++--- .../src/bl/payment_watcher.dart | 3 + .../bl/recover_cancel_tx_created_watcher.dart | 73 +++++++++++++++++++ .../bl/recover_cancel_tx_sent_watcher.dart | 67 +++++++++++++++++ .../src/bl/recover_pending_watcher.dart | 12 +-- .../src/bl/repository.dart | 63 +++++++++++++--- .../src/bl/tx_ready_watcher.dart | 2 +- .../src/widgets/oskp_screen.dart | 10 +++ 10 files changed, 304 insertions(+), 31 deletions(-) create mode 100644 packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_cancel_tx_created_watcher.dart create mode 100644 packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_cancel_tx_sent_watcher.dart diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart index 37dd4976a8..352d9c3770 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart @@ -78,14 +78,14 @@ class OSKPStatus with _$OSKPStatus { const factory OSKPStatus.cancelTxCreated( SignedTx tx, { required BigInt slot, - required EscrowPublicKey escrow, + required EscrowPrivateKey escrow, }) = OSKPStatusCancelTxCreated; /// There was an error while creating the cancellation tx, or the tx was /// rejected. It's safe to recreate it. const factory OSKPStatus.cancelTxFailure({ required TxFailureReason reason, - required EscrowPublicKey escrow, + required EscrowPrivateKey escrow, }) = OSKPStatusCancelTxFailure; /// Cancellation tx was sent but not confirmed yet. It's not safe to recreate @@ -93,10 +93,32 @@ class OSKPStatus with _$OSKPStatus { const factory OSKPStatus.cancelTxSent( SignedTx tx, { required BigInt slot, - required EscrowPublicKey escrow, + required EscrowPrivateKey escrow, }) = OSKPStatusCancelTxSent; + /// Tx was recovered from the blockchain. + /// These transactions cannot be resent, they can only be cancelled const factory OSKPStatus.recovered({ - required EscrowPublicKey escrow, + required EscrowPublicKey escrowPubKey, }) = OSKPStatusRecovered; + + /// Similar to [OSKPStatus.cancelTxCreated], but only contains public key + const factory OSKPStatus.recoveredCancelTxCreated( + SignedTx tx, { + required BigInt slot, + required EscrowPublicKey escrowPubKey, + }) = OSKPStatusRecoveredCancelTxCreated; + + /// Similar to [OSKPStatus.cancelTxSent], but only contains public key + const factory OSKPStatus.recoveredCancelTxSent( + SignedTx tx, { + required BigInt slot, + required EscrowPublicKey escrowPubKey, + }) = OSKPStatusRecoveredCancelTxSent; + + /// Similar to [OSKPStatus.cancelTxFailure], but only contains public key + const factory OSKPStatus.recoveredCancelTxFailure({ + required TxFailureReason reason, + required EscrowPublicKey escrowPubKey, + }) = OSKPStatusRecoveredCancelTxFailure; } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart index 28bb2e29c4..efff98de7a 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart @@ -8,6 +8,8 @@ import '../../core/balances/context_ext.dart'; import '../../di.dart'; import 'src/bl/cancel_tx_created_watcher.dart'; import 'src/bl/cancel_tx_sent_watcher.dart'; +import 'src/bl/recover_cancel_tx_created_watcher.dart'; +import 'src/bl/recover_cancel_tx_sent_watcher.dart'; import 'src/bl/recover_pending_watcher.dart'; import 'src/bl/repository.dart'; import 'src/bl/tx_confirmed_watcher.dart'; @@ -58,6 +60,18 @@ class OSKPModule extends SingleChildStatelessWidget { ..call(onBalanceAffected: () => context.notifyBalanceAffected()), dispose: (_, value) => value.dispose(), ), + Provider( + lazy: false, + create: (context) => sl() + ..call(onBalanceAffected: () => context.notifyBalanceAffected()), + dispose: (_, value) => value.dispose(), + ), + Provider( + lazy: false, + create: (context) => sl() + ..call(onBalanceAffected: () => context.notifyBalanceAffected()), + dispose: (_, value) => value.dispose(), + ), Provider( lazy: false, create: (context) => sl( diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart index 9357168ce2..a0e23a5c9d 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/oskp_service.dart @@ -77,12 +77,15 @@ class OSKPService { txId: null, timestamp: DateTime.now(), ); + } else if (status is OSKPStatusRecovered) { + newStatus = await _createRecoveredCancelTx( + escrow: status.escrowPubKey, + account: account, + ); } else { - final escrow = await status.mapOrNull( - linksReady: (it) async => it.escrow.keyPair - .then((e) => Ed25519HDPublicKey.fromBase58(e.address)), - recovered: (it) async => it.escrow, - cancelTxFailure: (it) async => it.escrow, + final escrow = status.mapOrNull( + linksReady: (it) => it.escrow, + cancelTxFailure: (it) => it.escrow, ); if (escrow == null) { @@ -90,7 +93,9 @@ class OSKPService { } newStatus = await _createCancelTx( - escrow: escrow, + escrow: await Ed25519HDKeyPair.fromPrivateKeyBytes( + privateKey: escrow.bytes, + ), account: account, apiVersion: payment.apiVersion, ); @@ -142,14 +147,16 @@ class OSKPService { } Future _createCancelTx({ - required EscrowPublicKey escrow, + required Ed25519HDKeyPair escrow, required ECWallet account, required SplitKeyApiVersion apiVersion, }) async { + final privateKey = await EscrowPrivateKey.fromKeyPair(escrow); + try { final dto = CancelPaymentRequestDto( senderAccount: account.address, - escrowAccount: escrow.toBase58(), + escrowAccount: escrow.address, cluster: apiCluster, ); @@ -161,7 +168,7 @@ class OSKPService { case SplitKeyApiVersion.manual: final dto = ReceivePaymentRequestDto( receiverAccount: account.address, - escrowAccount: escrow.toBase58(), + escrowAccount: escrow.address, cluster: apiCluster, ); final response = await _client.receivePayment(dto); @@ -183,12 +190,44 @@ class OSKPService { return OSKPStatus.cancelTxCreated( tx, - escrow: escrow, + escrow: privateKey, slot: slot, ); } on Exception { return OSKPStatus.cancelTxFailure( - escrow: escrow, + escrow: privateKey, + reason: TxFailureReason.creatingFailure, + ); + } + } + + Future _createRecoveredCancelTx({ + required EscrowPublicKey escrow, + required ECWallet account, + }) async { + try { + final dto = CancelPaymentRequestDto( + senderAccount: account.address, + escrowAccount: escrow.toBase58(), + cluster: apiCluster, + ); + + final response = await _client.cancelPaymentEc(dto); + + final transaction = response.transaction; + final slot = response.slot; + final tx = await transaction + .let(SignedTx.decode) + .let((it) => it.resign(account)); + + return OSKPStatus.recoveredCancelTxCreated( + tx, + escrowPubKey: escrow, + slot: slot, + ); + } on Exception { + return OSKPStatus.recoveredCancelTxFailure( + escrowPubKey: escrow, reason: TxFailureReason.creatingFailure, ); } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart index 13fb5200cb..56e42276bc 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/payment_watcher.dart @@ -79,5 +79,8 @@ extension on OSKPStatus { cancelTxFailure: F, cancelTxSent: F, recovered: F, + recoveredCancelTxCreated: F, + recoveredCancelTxSent: F, + recoveredCancelTxFailure: F, ); } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_cancel_tx_created_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_cancel_tx_created_watcher.dart new file mode 100644 index 0000000000..ec89dbff59 --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_cancel_tx_created_watcher.dart @@ -0,0 +1,73 @@ +import 'dart:async'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../../core/cancelable_job.dart'; +import '../../../../core/transactions/tx_sender.dart'; +import '../../models/outgoing_split_key_payment.dart'; +import 'payment_watcher.dart'; +import 'repository.dart'; + +/// Watches for [OSKPStatus.recoveredCancelTxCreated] payments and sends the tx. +/// +/// The watcher will try to submit the tx until it's accepted or rejected. +@injectable +class RecoverCancelTxCreatedWatcher extends PaymentWatcher { + RecoverCancelTxCreatedWatcher(super._repository, this._sender); + + final TxSender _sender; + + @override + CancelableJob createJob( + OutgoingSplitKeyPayment payment, + ) => + _OSKPRecoverCancelTxCreatedJob(payment, _sender); + + @override + Stream> watchPayments( + OSKPRepository repository, + ) => + repository.watchRecoverCancelTxCreated(); +} + +class _OSKPRecoverCancelTxCreatedJob + extends CancelableJob { + _OSKPRecoverCancelTxCreatedJob(this.payment, this.sender); + + final OutgoingSplitKeyPayment payment; + final TxSender sender; + + @override + Future process() async { + final status = payment.status; + if (status is! OSKPStatusRecoveredCancelTxCreated) { + return payment; + } + + final tx = await sender.send(status.tx, minContextSlot: status.slot); + + final OSKPStatus? newStatus = tx.map( + sent: (_) => OSKPStatus.recoveredCancelTxSent( + status.tx, + escrowPubKey: status.escrowPubKey, + slot: status.slot, + ), + invalidBlockhash: (_) => OSKPStatus.recoveredCancelTxFailure( + reason: TxFailureReason.invalidBlockhashSending, + escrowPubKey: status.escrowPubKey, + ), + failure: (it) => OSKPStatus.recoveredCancelTxFailure( + reason: it.reason, + escrowPubKey: status.escrowPubKey, + ), + networkError: (_) => null, + ); + + if (newStatus == null) { + return null; + } + + return payment.copyWith(status: newStatus); + } +} diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_cancel_tx_sent_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_cancel_tx_sent_watcher.dart new file mode 100644 index 0000000000..92f6ff714f --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_cancel_tx_sent_watcher.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../../core/cancelable_job.dart'; +import '../../../../core/transactions/tx_sender.dart'; +import '../../models/outgoing_split_key_payment.dart'; +import 'payment_watcher.dart'; +import 'repository.dart'; + +/// Watches for [OSKPStatus.recoveredCancelTxSent] payments and waits for the tx to be +/// confirmed. +@injectable +class RecoverCancelTxSentWatcher extends PaymentWatcher { + RecoverCancelTxSentWatcher(super._repository, this._sender); + + final TxSender _sender; + + @override + CancelableJob createJob( + OutgoingSplitKeyPayment payment, + ) => + _OSKPRecoverCancelTxSentJob(payment, _sender); + + @override + Stream> watchPayments( + OSKPRepository repository, + ) => + repository.watchRecoverCancelTxSent(); +} + +class _OSKPRecoverCancelTxSentJob + extends CancelableJob { + _OSKPRecoverCancelTxSentJob(this.payment, this.sender); + + final OutgoingSplitKeyPayment payment; + final TxSender sender; + + @override + Future process() async { + final status = payment.status; + if (status is! OSKPStatusRecoveredCancelTxSent) { + return payment; + } + + final tx = await sender.wait(status.tx, minContextSlot: status.slot); + + final OSKPStatus? newStatus = tx.map( + success: (_) => OSKPStatus.canceled( + txId: status.tx.id, + timestamp: DateTime.now(), + ), + failure: (tx) => OSKPStatus.recoveredCancelTxFailure( + reason: tx.reason, + escrowPubKey: status.escrowPubKey, + ), + networkError: (_) => null, + ); + + if (newStatus == null) { + return null; + } + + return payment.copyWith(status: newStatus); + } +} diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart index 60a0817190..d9c1241899 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/recover_pending_watcher.dart @@ -96,7 +96,7 @@ class RecoverPendingWatcher { value: amount, cryptoCurrency: const CryptoCurrency(token: Token.usdc), ), - status: OSKPStatus.recovered(escrow: escrow), + status: OSKPStatus.recovered(escrowPubKey: escrow), created: timestamp, linksGeneratedAt: timestamp, apiVersion: SplitKeyApiVersion.smartContract, @@ -119,10 +119,12 @@ class RecoverPendingWatcher { txSent: (it) async => it.escrow.keyPair.then((v) => v.publicKey), txConfirmed: (it) async => it.escrow.keyPair.then((v) => v.publicKey), linksReady: (it) async => it.escrow.keyPair.then((v) => v.publicKey), - cancelTxCreated: (it) async => it.escrow, - cancelTxFailure: (it) async => it.escrow, - cancelTxSent: (it) async => it.escrow, - recovered: (it) async => it.escrow, + cancelTxCreated: (it) async => + it.escrow.keyPair.then((v) => v.publicKey), + cancelTxFailure: (it) async => + it.escrow.keyPair.then((v) => v.publicKey), + cancelTxSent: (it) async => it.escrow.keyPair.then((v) => v.publicKey), + recovered: (it) async => it.escrowPubKey, ); if (escrow != null) { diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart index d7b8561812..d742a5a347 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/repository.dart @@ -106,6 +106,16 @@ class OSKPRepository { OSKPStatusDto.cancelTxWaitFailure, ]); + Stream> watchRecoverCancelTxCreated() => + _watchWithStatuses([ + OSKPStatusDto.recoveredCancelTxCreated, + ]); + + Stream> watchRecoverCancelTxSent() => + _watchWithStatuses([ + OSKPStatusDto.recoveredCancelTxSent, + ]); + Stream> watchPending() => _watchWithStatuses( OSKPStatusDto.values.whereNot( (e) => e == OSKPStatusDto.withdrawn || e == OSKPStatusDto.canceled, @@ -148,7 +158,7 @@ class OSKPRows extends Table with AmountMixin, EntityMixin { TextColumn get slot => text().nullable()(); IntColumn get apiVersion => intEnum().withDefault(const Constant(0))(); - TextColumn get publicKey => text().nullable()(); + TextColumn get publicKey => text().nullable()(); //recheck if needed } enum OSKPStatusDto { @@ -169,6 +179,9 @@ enum OSKPStatusDto { cancelTxSendFailure, cancelTxWaitFailure, recovered, + recoveredCancelTxFailure, + recoveredCancelTxSent, + recoveredCancelTxCreated, } extension OSKPRowExt on OSKPRow { @@ -197,7 +210,7 @@ extension on OSKPStatusDto { final txId = row.txId; final withdrawTxId = row.withdrawTxId; final escrow = row.privateKey?.let(base58decode).let(EscrowPrivateKey.new); - final escrowPubkey = await escrow?.keyPair.then((e) => e.publicKey); + final escrowPubkey = row.publicKey?.let(Ed25519HDPublicKey.fromBase58); final link1 = row.link1?.let(Uri.parse); final link2 = row.link2?.let(Uri.parse); final link3 = row.link3?.let(Uri.tryParse); @@ -264,35 +277,52 @@ extension on OSKPStatusDto { case OSKPStatusDto.cancelTxCreated: return OSKPStatus.cancelTxCreated( cancelTx!, - escrow: escrowPubkey!, + escrow: escrow!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.cancelTxFailure: return OSKPStatus.cancelTxFailure( - escrow: escrowPubkey!, + escrow: escrow!, reason: row.txFailureReason ?? TxFailureReason.unknown, ); case OSKPStatusDto.cancelTxSent: return OSKPStatus.cancelTxSent( cancelTx!, - escrow: escrowPubkey!, + escrow: escrow!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.cancelTxSendFailure: return OSKPStatus.cancelTxCreated( cancelTx!, - escrow: escrowPubkey!, + escrow: escrow!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.cancelTxWaitFailure: return OSKPStatus.cancelTxSent( cancelTx!, - escrow: escrowPubkey!, + escrow: escrow!, slot: slot ?? BigInt.zero, ); case OSKPStatusDto.recovered: return OSKPStatus.recovered( - escrow: Ed25519HDPublicKey.fromBase58(row.publicKey!), + escrowPubKey: escrowPubkey!, + ); + case OSKPStatusDto.recoveredCancelTxFailure: + return OSKPStatus.recoveredCancelTxFailure( + escrowPubKey: escrowPubkey!, + reason: row.txFailureReason ?? TxFailureReason.unknown, + ); + case OSKPStatusDto.recoveredCancelTxSent: + return OSKPStatus.recoveredCancelTxSent( + cancelTx!, + escrowPubKey: escrowPubkey!, + slot: slot ?? BigInt.zero, + ); + case OSKPStatusDto.recoveredCancelTxCreated: + return OSKPStatus.recoveredCancelTxCreated( + cancelTx!, + escrowPubKey: escrowPubkey!, + slot: slot ?? BigInt.zero, ); } } @@ -336,6 +366,11 @@ extension on OSKPStatus { cancelTxFailure: always(OSKPStatusDto.cancelTxFailure), cancelTxSent: always(OSKPStatusDto.cancelTxSent), recovered: always(OSKPStatusDto.recovered), + recoveredCancelTxFailure: + always(OSKPStatusDto.recoveredCancelTxFailure), + recoveredCancelTxSent: always(OSKPStatusDto.recoveredCancelTxSent), + recoveredCancelTxCreated: + always(OSKPStatusDto.recoveredCancelTxCreated), ); String? toTx() => mapOrNull( @@ -353,12 +388,14 @@ extension on OSKPStatus { String? toCancelTx() => mapOrNull( cancelTxCreated: (it) => it.tx.encode(), cancelTxSent: (it) => it.tx.encode(), + recoveredCancelTxCreated: (it) => it.tx.encode(), ); String? toCancelTxId() => mapOrNull( cancelTxCreated: (it) => it.tx.id, cancelTxSent: (it) => it.tx.id, canceled: (it) => it.txId, + recoveredCancelTxCreated: (it) => it.tx.id, ); Future toPrivateKey() async => this.map( @@ -369,10 +406,13 @@ extension on OSKPStatus { withdrawn: (it) async => null, canceled: (it) async => null, txFailure: (it) async => null, - cancelTxCreated: (it) async => base58encode(it.escrow.bytes), + cancelTxCreated: (it) async => null, cancelTxFailure: (it) async => base58encode(it.escrow.bytes), cancelTxSent: (it) async => base58encode(it.escrow.bytes), recovered: (it) async => null, + recoveredCancelTxFailure: (it) async => null, + recoveredCancelTxSent: (it) async => null, + recoveredCancelTxCreated: (it) async => null, ); String? toLink1() => mapOrNull( @@ -405,7 +445,10 @@ extension on OSKPStatus { ); String? toPublicKey() => mapOrNull( - recovered: (it) => it.escrow.toBase58(), + recovered: (it) => it.escrowPubKey.toBase58(), + recoveredCancelTxCreated: (it) => it.escrowPubKey.toBase58(), + recoveredCancelTxSent: (it) => it.escrowPubKey.toBase58(), + recoveredCancelTxFailure: (it) => it.escrowPubKey.toBase58(), ); } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart index bc68a558be..f87879296d 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/bl/tx_ready_watcher.dart @@ -78,7 +78,7 @@ class TxReadyWatcher { final escrowAccount = await status.mapOrNull( linksReady: (it) async => it.escrow.keyPair .then((e) => Ed25519HDPublicKey.fromBase58(e.address)), - recovered: (it) async => it.escrow, + recovered: (it) async => it.escrowPubKey, ); if (escrowAccount == null) continue; diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart index 4ad7c9ad62..9765429c27 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/src/widgets/oskp_screen.dart @@ -148,6 +148,9 @@ class _OSKPScreenState extends State { cancelTxFailure: always(CpStatusType.error), cancelTxSent: always(CpStatusType.info), recovered: always(CpStatusType.info), + recoveredCancelTxFailure: always(CpStatusType.error), + recoveredCancelTxCreated: always(CpStatusType.info), + recoveredCancelTxSent: always(CpStatusType.info), ); final String? statusTitle = payment.status.mapOrNull( @@ -185,6 +188,7 @@ class _OSKPScreenState extends State { canceled: always(CpTimelineStatus.neutral), txFailure: always(CpTimelineStatus.failure), cancelTxFailure: always(CpTimelineStatus.failure), + recoveredCancelTxFailure: always(CpTimelineStatus.failure), ) ?? CpTimelineStatus.inProgress; @@ -200,6 +204,9 @@ class _OSKPScreenState extends State { linksReady: always(1), withdrawn: always(2), recovered: always(1), + recoveredCancelTxFailure: always(0), + recoveredCancelTxCreated: always(1), + recoveredCancelTxSent: always(1), ); final paymentInitiated = CpTimelineItem( @@ -237,6 +244,9 @@ class _OSKPScreenState extends State { cancelTxCreated: always(cancelingItems), cancelTxFailure: always(cancelingItems), cancelTxSent: always(cancelingItems), + recoveredCancelTxCreated: always(cancelingItems), + recoveredCancelTxFailure: always(cancelingItems), + recoveredCancelTxSent: always(cancelingItems), ) ?? normalItems; From c4d16ca55c16130f5b12127389014cc8038c3b05 Mon Sep 17 00:00:00 2001 From: Justin Enerio Date: Thu, 3 Aug 2023 17:36:06 +0800 Subject: [PATCH 23/23] upd code style --- .../data/repository.dart | 2 +- .../recover_cancel_tx_created_watcher.dart | 12 +- .../recover_cancel_tx_sent_watcher.dart | 12 +- .../services/recover_pending_watcher.dart | 105 +++++++++--------- 4 files changed, 58 insertions(+), 73 deletions(-) diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/data/repository.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/data/repository.dart index e26ed9cf49..45c905c308 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/data/repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/data/repository.dart @@ -160,7 +160,7 @@ class OSKPRows extends Table with AmountMixin, EntityMixin { TextColumn get slot => text().nullable()(); IntColumn get apiVersion => intEnum().withDefault(const Constant(0))(); - TextColumn get publicKey => text().nullable()(); //recheck if needed + TextColumn get publicKey => text().nullable()(); } enum OSKPStatusDto { diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_created_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_created_watcher.dart index 4eed8d3467..d0bb287268 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_created_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_created_watcher.dart @@ -34,7 +34,7 @@ class RecoverCancelTxCreatedWatcher extends PaymentWatcher { class _OSKPRecoverCancelTxCreatedJob extends CancelableJob { - _OSKPRecoverCancelTxCreatedJob(this.payment, this.sender); + const _OSKPRecoverCancelTxCreatedJob(this.payment, this.sender); final OutgoingSplitKeyPayment payment; final TxSender sender; @@ -42,9 +42,7 @@ class _OSKPRecoverCancelTxCreatedJob @override Future process() async { final status = payment.status; - if (status is! OSKPStatusRecoveredCancelTxCreated) { - return payment; - } + if (status is! OSKPStatusRecoveredCancelTxCreated) return payment; final tx = await sender.send(status.tx, minContextSlot: status.slot); @@ -65,10 +63,6 @@ class _OSKPRecoverCancelTxCreatedJob networkError: (_) => null, ); - if (newStatus == null) { - return null; - } - - return payment.copyWith(status: newStatus); + return newStatus == null ? null : payment.copyWith(status: newStatus); } } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_sent_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_sent_watcher.dart index 8b7db4ae60..5566d7de6c 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_sent_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_sent_watcher.dart @@ -32,7 +32,7 @@ class RecoverCancelTxSentWatcher extends PaymentWatcher { class _OSKPRecoverCancelTxSentJob extends CancelableJob { - _OSKPRecoverCancelTxSentJob(this.payment, this.sender); + const _OSKPRecoverCancelTxSentJob(this.payment, this.sender); final OutgoingSplitKeyPayment payment; final TxSender sender; @@ -40,9 +40,7 @@ class _OSKPRecoverCancelTxSentJob @override Future process() async { final status = payment.status; - if (status is! OSKPStatusRecoveredCancelTxSent) { - return payment; - } + if (status is! OSKPStatusRecoveredCancelTxSent) return payment; final tx = await sender.wait(status.tx, minContextSlot: status.slot); @@ -58,10 +56,6 @@ class _OSKPRecoverCancelTxSentJob networkError: (_) => null, ); - if (newStatus == null) { - return null; - } - - return payment.copyWith(status: newStatus); + return newStatus == null ? null : payment.copyWith(status: newStatus); } } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_pending_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_pending_watcher.dart index 110a312fad..c58cd2308c 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_pending_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_pending_watcher.dart @@ -17,7 +17,7 @@ import '../models/outgoing_split_key_payment.dart'; @injectable class RecoverPendingWatcher { - RecoverPendingWatcher( + const RecoverPendingWatcher( this._client, this._repository, { @factoryParam required Ed25519HDPublicKey userPublicKey, @@ -49,61 +49,58 @@ class RecoverPendingWatcher { Ed25519HDPublicKey.fromBase58(escrowScAddress), ); - if (hasInteractedWithEscrow) { - // Find the escrow address from accounts. It should either be in index 1 or 2. - // Index 0 is the platforms account, index 1 or 2 should either be the user or the escrow. - final escrow = accounts - .getRange(1, 2) - .where((e) => e != _userPublicKey) - .firstOrNull; - - if (escrow != null) { - if (pendingEscrows.contains(escrow)) continue; - - final txList = await _client.rpcClient.getTransactionsList( - escrow, - limit: 2, - commitment: Commitment.confirmed, - encoding: Encoding.jsonParsed, - ); - - if (txList.length < 2) { - final id = const Uuid().v4(); - - final tx = txList.first; - - int amount = 0; - - for (final ix - in tx.meta?.innerInstructions?.last.instructions ?? []) { - if (ix is ParsedInstructionSplToken && - ix.parsed is ParsedSplTokenTransferInstruction) { - final parsed = ix.parsed as ParsedSplTokenTransferInstruction; - - amount = int.parse(parsed.info.amount); - } - } - - final timestamp = detail.blockTime?.let( - (it) => DateTime.fromMillisecondsSinceEpoch(it * 1000), - ) ?? - DateTime.now(); - - await _repository.save( - OutgoingSplitKeyPayment( - id: id, - amount: CryptoAmount( - value: amount, - cryptoCurrency: const CryptoCurrency(token: Token.usdc), - ), - status: OSKPStatus.recovered(escrowPubKey: escrow), - created: timestamp, - linksGeneratedAt: timestamp, - apiVersion: SplitKeyApiVersion.smartContract, - ), - ); + if (!hasInteractedWithEscrow) continue; + + // Find the escrow address from accounts. It should either be in index 1 or 2. + // Index 0 is the platforms account, index 1 or 2 should either be the user or the escrow. + final escrow = + accounts.getRange(1, 2).where((e) => e != _userPublicKey).firstOrNull; + + if (escrow == null) continue; + + if (pendingEscrows.contains(escrow)) continue; + + final txList = await _client.rpcClient.getTransactionsList( + escrow, + limit: 2, + commitment: Commitment.confirmed, + encoding: Encoding.jsonParsed, + ); + + if (txList.length < 2) { + final id = const Uuid().v4(); + + final tx = txList.first; + + int amount = 0; + + for (final ix in tx.meta?.innerInstructions?.last.instructions ?? []) { + if (ix is ParsedInstructionSplToken && + ix.parsed is ParsedSplTokenTransferInstruction) { + final parsed = ix.parsed as ParsedSplTokenTransferInstruction; + + amount = int.parse(parsed.info.amount); } } + + final timestamp = detail.blockTime?.let( + (it) => DateTime.fromMillisecondsSinceEpoch(it * 1000), + ) ?? + DateTime.now(); + + await _repository.save( + OutgoingSplitKeyPayment( + id: id, + amount: CryptoAmount( + value: amount, + cryptoCurrency: const CryptoCurrency(token: Token.usdc), + ), + status: OSKPStatus.recovered(escrowPubKey: escrow), + created: timestamp, + linksGeneratedAt: timestamp, + apiVersion: SplitKeyApiVersion.smartContract, + ), + ); } } }