diff --git a/backup_restore/pubspec.lock b/backup_restore/pubspec.lock index d61f9c8..1c2e65a 100644 --- a/backup_restore/pubspec.lock +++ b/backup_restore/pubspec.lock @@ -28,7 +28,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" base32: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -217,7 +217,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: @@ -348,7 +348,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" toml: dependency: transitive description: diff --git a/base/lib/base/kin_account_context.dart b/base/lib/base/kin_account_context.dart index bbe4d48..da45a6d 100644 --- a/base/lib/base/kin_account_context.dart +++ b/base/lib/base/kin_account_context.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:decimal/decimal.dart'; import 'package:kin_base/base/kin_environment.dart'; import 'package:kin_base/base/models/kin_balance.dart'; +import 'package:kin_base/base/models/solana/instruction.dart'; import 'package:kin_base/base/models/stellar_base_type_conversions.dart'; import 'package:kin_base/base/network/api/agora/model_to_proto.dart'; import 'package:kin_base/base/network/services/kin_service.dart'; @@ -65,9 +66,8 @@ abstract class KinPaymentReadOperationsAltIdioms { abstract class KinPaymentReadOperations implements KinPaymentReadOperationsAltIdioms { - - Future calculateFee(int numberOfOperations); - + //TODO: Observe Payment + //TODO: GetPaymentsForTransactionHash } abstract class KinPaymentWriteOperationsAltIdioms { @@ -257,23 +257,6 @@ class KinAccountContextBase implements KinAccountReadOperations , KinPaymentRead return account.balance; } - @override - Future calculateFee(int numberOfOperations) async { - if (await service.canWhitelistTransactions()) { - return KinAmount.zero.toQuarks(); - } - else { - var minFee = await storage.getMinFee(); - - if (minFee == null) { - minFee = await service.getMinFee(); - await storage.setMinFee(minFee); - } - - return QuarkAmount(minFee.value! * numberOfOperations); - } - } - @override Future clearStorage({Callback? clearCompleteCallback}) async { log!.log("clearStorage"); @@ -398,7 +381,7 @@ class KinAccountContextBase implements KinAccountReadOperations , KinPaymentRead catch(e) { var accounts = await service.resolveTokenAccounts(accountId); - var maybeResolvedAccountId = accounts.isNotEmpty ? accounts.first.asKinAccountId() : accountId ; + var maybeResolvedAccountId = accounts.isNotEmpty ? accounts.first.key.asKinAccountId() : accountId ; var account2 = await service.getAccount(maybeResolvedAccountId) ; @@ -408,7 +391,7 @@ class KinAccountContextBase implements KinAccountReadOperations , KinPaymentRead accountResolved = account2!.copy( id: accountId, key: PublicKey.fromBytes(accountId.value), - tokenAccounts: accounts, + tokenAccounts: accounts.map((e) => e.key).toList(), ); } else { @@ -490,7 +473,7 @@ class KinAccountContextImpl extends KinAccountContextBase with KinAccountContext AppInfoProvider? appInfoProvider; KinAccountContextImpl._(executors, KinService service, Storage storage, - KinAccountId accountId, this.appInfoProvider, KinLoggerFactory logger) + KinAccountId accountId, this.appInfoProvider, KinLoggerFactory logger, bool shouldAutoMergeTokenAccounts) : super(executors, service, storage, accountId, logger); static Uint8List? _generateRandomPrivateKey() { @@ -512,7 +495,8 @@ class KinAccountContextImpl extends KinAccountContextBase with KinAccountContext env.storage, newAccount.id, envAgora.appInfoProvider, - env.logger + env.logger, + env.shouldAutoMergeTokenAccounts ); } @@ -525,10 +509,28 @@ class KinAccountContextImpl extends KinAccountContextBase with KinAccountContext env.storage, accountId, envAgora.appInfoProvider, - env.logger + env.logger, + env.shouldAutoMergeTokenAccounts ); } + Future> mergeTokenAccountIfNecessary() async { + var account = await storage.getAccount(accountId); + var privateKey = account?.key as PrivateKey; + if(account?.status is KinAccountStatusRegistered && account != null && (account).tokenAccounts.length > 1) { + List tokenAccounts = List.empty(); + service.mergeTokenAccounts(accountId, privateKey, appInfoProvider!.appInfo!.appIndex).then((value) => + { + storage.updateAccountInStorage(account.copy(tokenAccounts: value.map((e) => e.key).toList())), + tokenAccounts = value + }); + return tokenAccounts; + } + else { + return List.empty(); + } + } + @override Future getAccount({bool forceUpdate = false, Callback? accountCallback}) async { log!.log("getAccount"); @@ -594,7 +596,7 @@ class KinAccountContextImpl extends KinAccountContextBase with KinAccountContext try { var resolveTokenAccounts = await service.resolveTokenAccounts(accountId); - var resolvedAccount = await storage.updateAccountInStorage(accountToStore.copy(tokenAccounts: resolveTokenAccounts)); + var resolvedAccount = await storage.updateAccountInStorage(accountToStore.copy(tokenAccounts: resolveTokenAccounts.map((e) => e.key).toList())); print('-- Resolved account: $resolvedAccount'); @@ -618,7 +620,7 @@ class KinAccountContextImpl extends KinAccountContextBase with KinAccountContext serviceExistingAccount = await service.getAccount(account.id); } catch(e) { - existingTokenAccounts = await service.resolveTokenAccounts(account.id); + existingTokenAccounts = (await service.resolveTokenAccounts(account.id)).map((e) => e.key).toList(); } return serviceExistingAccount != null || @@ -689,11 +691,6 @@ class KinAccountContextImpl extends KinAccountContextBase with KinAccountContext service.invalidateBlockhashCache(); continue; } - else if (e is InsufficientFeeInRequestError) { - var minFee = await service.getMinFee(); - storage.setMinFee(minFee); - continue; - } else if (e is UnknownAccountInRequestError) { var delay = invalidAccountErrorRetryStrategy.nextDelay()!; log!.log("Waiting $delay ms..."); @@ -724,7 +721,7 @@ class KinAccountContextImpl extends KinAccountContextBase with KinAccountContext } else { var resolveTokenAccounts = await service.resolveTokenAccounts(accountId); - var resolvedAccount = await storage.updateAccountInStorage(account!.copy(tokenAccounts: resolveTokenAccounts)); + var resolvedAccount = await storage.updateAccountInStorage(account!.copy(tokenAccounts: resolveTokenAccounts.map((e) => e.key).toList())); log!.log('_buildPaymentTransaction> account(resolved token account): $resolvedAccount'); @@ -741,11 +738,12 @@ class KinAccountContextImpl extends KinAccountContextBase with KinAccountContext } else { paymentItems = await Future.wait( payments.map((paymentItem) async { var destinationTokenAccounts = await service.resolveTokenAccounts(paymentItem.destinationAccount) ; - return paymentItem.copy(destinationAccount: destinationTokenAccounts.first.asKinAccountId()); + return paymentItem.copy(destinationAccount: destinationTokenAccounts.first.key.asKinAccountId()); })); } - var fee = await calculateFee(payments.length); + List createAccountInstructions = new List.empty(); + List additionalSigners = new List.empty(); var transaction = service.buildAndSignTransaction( sourceAccount.ownerKey, @@ -753,7 +751,8 @@ class KinAccountContextImpl extends KinAccountContextBase with KinAccountContext sourceAccount.nonce, paymentItems, memo, - feeOverride ?? fee + createAccountInstructions, + additionalSigners ); if (transaction is StellarKinTransaction) { diff --git a/base/lib/base/kin_environment.dart b/base/lib/base/kin_environment.dart index 0484563..93f07ea 100644 --- a/base/lib/base/kin_environment.dart +++ b/base/lib/base/kin_environment.dart @@ -27,9 +27,10 @@ class KinEnvironment { final Storage storage; final ExecutorServices executors; final NetworkOperationsHandler networkHandler; + final bool shouldAutoMergeTokenAccounts; KinEnvironment(this.networkEnvironment, this.logger, this.service, - this.storage, this.executors, this.networkHandler); + this.storage, this.executors, this.networkHandler, this.shouldAutoMergeTokenAccounts); Future importPrivateKey(PrivateKey privateKey, [ Callback? callback ]) async { var accountId = KinAccountId.fromPrivateKey(privateKey); @@ -76,6 +77,7 @@ class KinEnvironmentAgora extends KinEnvironment { final InMemoryInvoiceRepositoryImpl invoiceRepository = InMemoryInvoiceRepositoryImpl(); final AppInfoProvider appInfoProvider; + final bool shouldAutoMergeTokenAccounts; KinEnvironmentAgora( this.managedChannel, @@ -85,9 +87,10 @@ class KinEnvironmentAgora extends KinEnvironment { Storage storage, ExecutorServices executors, NetworkOperationsHandler networkHandler, + this.shouldAutoMergeTokenAccounts, this.appInfoProvider) : super(networkEnvironment, logger, service, storage, executors, - networkHandler); + networkHandler, shouldAutoMergeTokenAccounts); factory KinEnvironmentAgora.build(NetworkEnvironment networkEnvironment, { @@ -100,6 +103,7 @@ class KinEnvironmentAgora extends KinEnvironment { ExecutorServices? executors, NetworkOperationsHandler? networkHandler, AppInfoProvider? appInfoProvider, + bool? shouldAutoMergeTokenAccounts }) { logger ??= KinLoggerFactoryImpl(enableLogging) ; @@ -139,7 +143,9 @@ class KinEnvironmentAgora extends KinEnvironment { storage = storageBuilder!(networkEnvironment: networkEnvironment) ; } - var agora = KinEnvironmentAgora(managedChannel, networkEnvironment, logger, service, storage, executors, networkHandler, appInfoProvider); + shouldAutoMergeTokenAccounts ??= true; + + var agora = KinEnvironmentAgora(managedChannel, networkEnvironment, logger, service, storage, executors, networkHandler, shouldAutoMergeTokenAccounts, appInfoProvider); return agora ; } diff --git a/base/lib/base/models/kin_account.dart b/base/lib/base/models/kin_account.dart index bc81716..592dd91 100644 --- a/base/lib/base/models/kin_account.dart +++ b/base/lib/base/models/kin_account.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:kin_base/base/models/kin_amount.dart'; import 'package:kin_base/base/tools/base58.dart'; import 'package:kin_base/base/tools/extensions.dart'; import 'package:kin_base/stellarfork/key_pair.dart'; @@ -117,3 +118,11 @@ class KinAccount { return 'KinAccount{key: $key, id: $id, tokenAccounts: $tokenAccounts, balance: $balance, status: $status}'; } } + +class KinTokenAccountInfo{ + final PublicKey key; + final KinAmount balance; + final PublicKey? closeAuthority; + + KinTokenAccountInfo(this.key, this.balance, this.closeAuthority){} +} diff --git a/base/lib/base/models/solana/programs.dart b/base/lib/base/models/solana/programs.dart index 3ff3fb5..3f45dc3 100644 --- a/base/lib/base/models/solana/programs.dart +++ b/base/lib/base/models/solana/programs.dart @@ -1,12 +1,13 @@ import 'dart:convert'; import 'dart:typed_data'; - import 'package:kin_base/base/models/key.dart'; import 'package:kin_base/base/models/kin_amount.dart'; import 'package:kin_base/base/models/quark_amount.dart'; import 'package:kin_base/base/tools/base58.dart'; import 'package:kin_base/base/tools/byte_in_out_buffer.dart'; import 'package:kin_base/base/tools/extensions.dart'; +import 'package:kin_base/stellarfork/xdr/xdr_type.dart'; +import 'package:pointycastle/digests/sha256.dart'; import 'instruction.dart'; @@ -171,6 +172,113 @@ class CreateAccount { } } +final Uint8List SYS_VAR_RENT_KEY = Base58().decode("SysvarRent111111111111111111111111111111111"); + +class Address { + final int maxSeeds = 16; + final int maxSeedLength = 32; + + PublicKey? createProgramAddress(PublicKey program, Uint8List seeds) { + if(seeds.length > maxSeeds) { + throw Exception("too many seeds"); + } + + var h = SHA256Digest(); + + for (var s in seeds) { + if(s.bitLength > maxSeedLength) { + throw Exception("max seed length exceeded"); + } + try { + h.update(Uint8List.fromList([s]), 0, s.bitLength); + } catch (e) { + throw Exception("failed to hash seed " + e.toString()); + } + } + + for(var v in [program.value, "ProgramDerivedAddress".toBytesUTF8()]) { + try { + h.update(v ?? Uint8List.fromList(List.empty()), 0, v!.length); + } catch (e) { + throw Exception("failed to hash seed " + e.toString()); + } + } + + var pub; + h.doFinal(pub, 0); + + try { + // TEST the key curve + var a = XdrCurve25519Public(pub); + } catch (e) { + return PublicKey(pub); + } + throw Exception("invalid public key"); + } + + PublicKey? findProgramAddress(PublicKey program, Uint8List seeds) { + var maxUint8 = (1 << 8) - 1; + var bumpSeed = Uint8List(maxUint8); + + for(var i = 0; i < maxUint8; i++) { + try { + seeds.add(bumpSeed.single); + return createProgramAddress(program, seeds); + } catch (e) { + bumpSeed[0]--; + } + } + return null; + } +} + +class AssociatedTokenProgram { + static final PublicKey PROGRAM_KEY = PublicKey.fromBytes(Uint8List.fromList([ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ])); + + PublicKey? getAssociatedAccount(PublicKey wallet, PublicKey mint) { + return Address().findProgramAddress( + PROGRAM_KEY, + Uint8List.fromList([ + ...(wallet.value as Uint8List), + ...(TokenProgram.PROGRAM_KEY.value as Uint8List), + ...(mint.value as Uint8List) + ]) + ); + } +} + +class AssociatedTokenProgramCreateAssociatedTokenAccount { + final PublicKey subsidizer; + final PublicKey wallet; + final PublicKey mint; + + AssociatedTokenProgramCreateAssociatedTokenAccount( + this.subsidizer, this.wallet, this.mint); + + late final addr = AssociatedTokenProgram().getAssociatedAccount(wallet, mint) as PublicKey; + + Instruction? _instruction; + + Instruction? get instruction { + return _instruction ??= Instruction.newInstruction( + AssociatedTokenProgram.PROGRAM_KEY, + new Uint8List(0), [ + AccountMeta.newAccountMeta(subsidizer, true), + AccountMeta.newAccountMeta(addr, false), + AccountMeta.newReadonlyAccountMeta(wallet, false), + AccountMeta.newReadonlyAccountMeta(mint, false), + AccountMeta.newReadonlyAccountMeta(SystemProgram.PROGRAM_KEY, false), + AccountMeta.newReadonlyAccountMeta(TokenProgram.PROGRAM_KEY, false), + AccountMeta.newReadonlyAccountMeta(PublicKey.fromBytes(SYS_VAR_RENT_KEY), false) + ] + ); + } +} + class TokenProgram { // Reference: https://github.com/solana-labs/solana-program-library/blob/11b1e3eefdd4e523768d63f7c70a7aa391ea0d02/token/program/src/state.rs#L125 int accountSize = 165; @@ -186,9 +294,6 @@ class TokenProgram { 70, 206, 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, 245, 133, 126, 255, 0, 169 ])); - - static final Uint8List SYS_VAR_RENT = - Base58().decode("SysvarRent111111111111111111111111111111111"); } class TokenProgramCommand { @@ -353,17 +458,14 @@ class TokenProgramInitializeAccount { Instruction? _instruction; Instruction? get instruction { - if (_instruction == null) { - _instruction = Instruction.newInstruction(programKey, + return _instruction ??= Instruction.newInstruction(programKey, TokenProgramCommandInitializeAccount().value.toUint8List(), [ AccountMeta.newAccountMeta(account, true), AccountMeta.newReadonlyAccountMeta(mint, false), AccountMeta.newReadonlyAccountMeta(owner, false), AccountMeta.newReadonlyAccountMeta( - PublicKey.fromBytes(TokenProgram.SYS_VAR_RENT), false) + PublicKey.fromBytes(SYS_VAR_RENT_KEY), false) ]); - } - return _instruction; } } @@ -384,8 +486,7 @@ class TokenProgramTransfer { Instruction? _instruction; Instruction? get instruction { - if (_instruction == null) { - _instruction = Instruction.newInstruction( + return _instruction ??= Instruction.newInstruction( programKey, Uint8List.fromList([ TokenProgramCommandTransfer().value, @@ -396,8 +497,6 @@ class TokenProgramTransfer { AccountMeta.newAccountMeta(destination, false), AccountMeta.newAccountMeta(owner, true) ]); - } - return _instruction; } } @@ -479,6 +578,26 @@ class SetAuthority { } } +class CloseAccount { + final PublicKey account; + final PublicKey dest; + final PublicKey owner; + + CloseAccount(this.account, this.dest, this.owner); + + Instruction? _instruction; + + Instruction? get instruction { + return _instruction ??= Instruction.newInstruction( + TokenProgram.PROGRAM_KEY, + TokenProgramAuthorityTypeCloseAccount().value.toUint8List(), [ + AccountMeta.newAccountMeta(dest, false), + AccountMeta.newAccountMeta(dest, false), + AccountMeta.newReadonlyAccountMeta(owner, false) + ]); + } +} + class MemoProgram { static final MemoProgram _instance = MemoProgram._(); @@ -503,11 +622,8 @@ class MemoProgramBase64EncodedMemo extends MemoProgram { Instruction? _instruction; Instruction? get instruction { - if (_instruction == null) { - _instruction = Instruction.newInstruction( + return _instruction ??= Instruction.newInstruction( MemoProgram.PROGRAM_KEY, base64Value.toBytesUTF8()); - } - return _instruction; } } @@ -519,9 +635,6 @@ class MemoProgramRawMemo { Instruction? _instruction; Instruction? get instruction { - if (_instruction == null) { - _instruction = Instruction.newInstruction(MemoProgram.PROGRAM_KEY, bytes); - } - return _instruction; + return _instruction ??= Instruction.newInstruction(MemoProgram.PROGRAM_KEY, bytes); } } diff --git a/base/lib/base/network/api/agora/agora_v4_apis.dart b/base/lib/base/network/api/agora/agora_v4_apis.dart index 4187e9f..3f0294a 100644 --- a/base/lib/base/network/api/agora/agora_v4_apis.dart +++ b/base/lib/base/network/api/agora/agora_v4_apis.dart @@ -4,6 +4,7 @@ import 'package:grpc/grpc.dart'; import 'package:kin_base/base/models/invoices.dart'; import 'package:kin_base/base/models/key.dart'; import 'package:kin_base/base/models/kin_account.dart'; +import 'package:kin_base/base/models/kin_amount.dart'; import 'package:kin_base/base/models/quark_amount.dart'; import 'package:kin_base/base/models/solana/transaction.dart'; import 'package:kin_base/base/models/stellar_base_type_conversions.dart'; @@ -117,13 +118,21 @@ class AgoraKinAccountApiV4 extends GrpcApi implements KinAccountApiV4, KinStream } @override - Future>> resolveTokenAccounts(KinAccountId accountId) async { + Future>> resolveTokenAccounts(KinAccountId accountId) async { var request = new ResolveTokenAccountsRequest(accountId: accountId.toProtoSolanaAccountId(), includeAccountInfo: true); var accounts = await _accountClient.resolveTokenAccounts(request); - var publicKeys = accounts.tokenAccountInfos.map((e) => e.accountId.toPublicKey()).toList(); - - return new KinServiceResponse(KinServiceResponseType.ok, publicKeys); + var publicKeys = accounts.tokenAccountInfos; + + return new KinServiceResponse( + KinServiceResponseType.ok, + publicKeys.map((e) => + KinTokenAccountInfo( + PublicKey(e.accountId.toString()), + KinAmount.fromInt(e.balance.toInt()), + PublicKey(e.closeAuthority.toString()) + ) + ).toList()); } @override diff --git a/base/lib/base/network/api/kin_account_api_v4.dart b/base/lib/base/network/api/kin_account_api_v4.dart index 9ca0324..64c143b 100644 --- a/base/lib/base/network/api/kin_account_api_v4.dart +++ b/base/lib/base/network/api/kin_account_api_v4.dart @@ -7,6 +7,7 @@ import 'kin_account_api.dart'; abstract class KinAccountApiV4 extends KinAccountApi { - Future>> resolveTokenAccounts(KinAccountId accountId) ; + + Future>> resolveTokenAccounts(KinAccountId accountId) ; } diff --git a/base/lib/base/network/services/kin_service.dart b/base/lib/base/network/services/kin_service.dart index 0464d76..15c76af 100644 --- a/base/lib/base/network/services/kin_service.dart +++ b/base/lib/base/network/services/kin_service.dart @@ -1,35 +1,41 @@ import 'dart:async'; +import 'package:kin_base/base/models/appidx.dart'; import 'package:kin_base/base/models/key.dart'; import 'package:kin_base/base/models/kin_account.dart'; import 'package:kin_base/base/models/kin_memo.dart'; import 'package:kin_base/base/models/kin_payment_item.dart'; -import 'package:kin_base/base/models/quark_amount.dart'; +import 'package:kin_base/base/models/solana/instruction.dart'; import 'package:kin_base/base/models/transaction_hash.dart'; import 'package:kin_base/base/stellar/models/kin_transaction.dart'; import 'package:kin_base/base/stellar/models/paging_token.dart'; import 'package:kin_base/base/tools/observers.dart'; abstract class KinService { - Future createAccount(KinAccountId accountId, PrivateKey signer) ; + Future createAccount(KinAccountId accountId, PrivateKey signer); - Future getAccount(KinAccountId accountId) ; + Future getAccount(KinAccountId accountId); - Future> resolveTokenAccounts(KinAccountId accountId) ; + Future> resolveTokenAccounts(KinAccountId accountId); - Future?> getLatestTransactions(KinAccountId kinAccountId) ; + Future> mergeTokenAccounts( + KinAccountId accountId, + PrivateKey signer, + AppIdx appIdx, + [bool shouldCreateAssociatedAccount = true] + ); + + Future?> getLatestTransactions(KinAccountId kinAccountId); Future?> getTransactionPage( KinAccountId kinAccountId, PagingToken? pagingToken, KinServiceOrder order - ) ; - - Future getTransaction(TransactionHash transactionHash) ; + ); - FutureOr canWhitelistTransactions() ; + Future getTransaction(TransactionHash transactionHash); - Future getMinFee() ; + FutureOr canWhitelistTransactions(); Future buildAndSignTransaction( PrivateKey ownerKey, @@ -37,25 +43,26 @@ abstract class KinService { int nonce, List paymentItems, KinMemo? memo, - QuarkAmount fee - ) ; + List createAccountInstructions, + List signers + ); - Future submitTransaction(KinTransaction transaction) ; + Future submitTransaction(KinTransaction transaction); Future buildSignAndSubmitTransaction( Future Function() buildAndSignTransaction - ) ; + ); - Observer streamAccount(KinAccountId kinAccountId) ; + Observer streamAccount(KinAccountId kinAccountId); - Observer streamNewTransactions(KinAccountId kinAccountId) ; + Observer streamNewTransactions(KinAccountId kinAccountId); - void invalidateBlockhashCache() ; + void invalidateBlockhashCache(); } enum KinServiceOrder { ascending, descending } -typedef KinServiceResponseCallback = void Function(KinServiceResponse response) ; +typedef KinServiceResponseCallback = void Function(KinServiceResponse response); class KinServiceResponse

{ final KinServiceResponseType type; diff --git a/base/lib/base/network/services/kin_service_impl.dart b/base/lib/base/network/services/kin_service_impl.dart deleted file mode 100644 index 486da54..0000000 --- a/base/lib/base/network/services/kin_service_impl.dart +++ /dev/null @@ -1,126 +0,0 @@ - - -import 'dart:async'; - -import 'package:kin_base/base/models/key.dart'; -import 'package:kin_base/base/models/kin_account.dart'; -import 'package:kin_base/base/models/kin_memo.dart'; -import 'package:kin_base/base/models/kin_payment_item.dart'; -import 'package:kin_base/base/models/quark_amount.dart'; -import 'package:kin_base/base/models/transaction_hash.dart'; -import 'package:kin_base/base/network/api/kin_transaction_api.dart'; -import 'package:kin_base/base/network/api/kin_account_api.dart'; -import 'package:kin_base/base/network/api/kin_streaming_api.dart'; -import 'package:kin_base/base/network/api/kin_account_creation_api.dart'; -import 'package:kin_base/base/network/api/kin_transaction_whitelisting_api.dart'; -import 'package:kin_base/base/stellar/models/kin_transaction.dart'; -import 'package:kin_base/base/stellar/models/network_environment.dart'; -import 'package:kin_base/base/stellar/models/paging_token.dart'; -import 'package:kin_base/base/tools/kin_logger.dart'; -import 'package:kin_base/base/tools/network_operations_handler.dart'; -import 'package:kin_base/base/tools/observers.dart'; - -import 'kin_service.dart'; - -class KinServiceImpl extends KinService { - - final NetworkEnvironment networkEnvironment ; - final NetworkOperationsHandler networkOperationsHandler ; - final KinAccountApi accountApi ; - final KinTransactionApi transactionApi ; - final KinStreamingApi streamingApi ; - final KinAccountCreationApi accountCreationApi ; - final KinTransactionWhitelistingApi transactionWhitelistingApi ; - final KinLoggerFactory logger; - - KinServiceImpl( - this.networkEnvironment, - this.networkOperationsHandler, - this.accountApi, - this.transactionApi, - this.streamingApi, - this.accountCreationApi, - this.transactionWhitelistingApi, - this.logger); - - @override - Future buildAndSignTransaction(PrivateKey ownerKey, PublicKey sourceKey, int nonce, List paymentItems, KinMemo? memo, QuarkAmount fee) { - // TODO: implement buildAndSignTransaction - throw UnimplementedError(); - } - - @override - Future buildSignAndSubmitTransaction(Future Function() buildAndSignTransaction) async { - var ret = await buildAndSignTransaction(); - return submitTransaction(ret); - } - - @override - FutureOr canWhitelistTransactions() => transactionWhitelistingApi.isWhitelistingAvailable; - - @override - Future createAccount(KinAccountId accountId, PrivateKey signer) { - // TODO: implement createAccount - throw UnimplementedError(); - } - - @override - Future getAccount(KinAccountId accountId) { - // TODO: implement getAccount - throw UnimplementedError(); - } - - @override - Future> getLatestTransactions(KinAccountId kinAccountId) { - // TODO: implement getLatestTransactions - throw UnimplementedError(); - } - - @override - Future getMinFee() { - // TODO: implement getMinFee - throw UnimplementedError(); - } - - @override - Future getTransaction(TransactionHash transactionHash) { - // TODO: implement getTransaction - throw UnimplementedError(); - } - - @override - Future> getTransactionPage(KinAccountId kinAccountId, PagingToken? pagingToken, KinServiceOrder order) { - // TODO: implement getTransactionPage - throw UnimplementedError(); - } - - @override - void invalidateBlockhashCache() { - // TODO: implement invalidateBlockhashCache - throw UnimplementedError(); - } - - @override - Future> resolveTokenAccounts(KinAccountId accountId) { - // TODO: implement resolveTokenAccounts - throw UnimplementedError(); - } - - @override - Observer streamAccount(KinAccountId kinAccountId) { - // TODO: implement streamAccount - throw UnimplementedError(); - } - - @override - Observer streamNewTransactions(KinAccountId kinAccountId) { - // TODO: implement streamNewTransactions - throw UnimplementedError(); - } - - @override - Future submitTransaction(KinTransaction transaction) { - // TODO: implement submitTransaction - throw UnimplementedError(); - } -} \ No newline at end of file diff --git a/base/lib/base/network/services/kin_service_impl_v4.dart b/base/lib/base/network/services/kin_service_impl_v4.dart index 4a5e5fd..c6ba218 100644 --- a/base/lib/base/network/services/kin_service_impl_v4.dart +++ b/base/lib/base/network/services/kin_service_impl_v4.dart @@ -1,11 +1,14 @@ import 'dart:async'; +import 'package:kin_base/base/models/appidx.dart'; import 'package:kin_base/base/models/key.dart'; import 'package:kin_base/base/models/kin_account.dart'; +import 'package:kin_base/base/models/kin_binary_memo.dart'; import 'package:kin_base/base/models/kin_memo.dart'; import 'package:kin_base/base/models/kin_payment_item.dart'; import 'package:kin_base/base/models/quark_amount.dart'; +import 'package:kin_base/base/models/solana/instruction.dart'; import 'package:kin_base/base/models/solana/programs.dart'; import 'package:kin_base/base/models/solana/transaction.dart'; import 'package:kin_base/base/models/stellar_base_type_conversions.dart'; @@ -135,8 +138,8 @@ class KinServiceImplV4 extends KinService { } @override - Future buildAndSignTransaction(PrivateKey ownerKey, PublicKey sourceKey, int nonce, List paymentItems, KinMemo? memo, QuarkAmount fee) { - log!.log("buildAndSignTransaction: ownerKey: $ownerKey ; sourceKey: $sourceKey ; nonce: $nonce ; paymentItems: $paymentItems ; memo: $memo ; fee:$fee"); + Future buildAndSignTransaction(PrivateKey ownerKey, PublicKey sourceKey, int nonce, List paymentItems, KinMemo? memo, List createAccountInstructions, List additionalSigners) { + log!.log("buildAndSignTransaction: ownerKey: $ownerKey ; sourceKey: $sourceKey ; paymentItems: $paymentItems ; memo: $memo ;"); return networkOperationsHandler.queueWork('buildAndSignTransaction', () async { ServiceConfig? serviceConfig ; @@ -179,7 +182,7 @@ class KinServiceImplV4 extends KinService { var tx = Transaction.newTransaction( subsidizer, - [memoInstruction, ...(paymentInstructions.toList())].whereNotNull(), + [memoInstruction, ...(paymentInstructions.toList()), ...(createAccountInstructions.toList())].whereNotNull(), ).copyAndSetRecentBlockhash(recentBlockHash).copyAndSign([ownerKey]); var kinTransaction = SolanaKinTransaction( @@ -316,12 +319,6 @@ class KinServiceImplV4 extends KinService { }); } - @override - Future getMinFee() { - // TODO: implement getMinFee - throw UnimplementedError(); - } - @override Future getTransaction(TransactionHash transactionHash) async { return networkOperationsHandler.queueWork('KinServiceImplV4.getTransaction', () async { @@ -377,11 +374,16 @@ class KinServiceImplV4 extends KinService { _cache.invalidate("recentBlockHash"); } + void invalidateResolvedTokenAccounts(KinAccountId accountId) { + var cacheKey = "resolvedAccounts:${accountId.base58Encode()}"; + _cache.invalidate(cacheKey); + } + @override - Future> resolveTokenAccounts(KinAccountId accountId) async { + Future> resolveTokenAccounts(KinAccountId accountId) async { var cacheKey = "resolvedAccounts:${accountId.stellarBase32Encode()}"; - List? tokenAccounts; + List? tokenAccounts; try { tokenAccounts = await _cache.resolve(cacheKey, fault: (k) async { @@ -408,7 +410,7 @@ class KinServiceImplV4 extends KinService { }); }); - return tokenAccounts ?? []; + return tokenAccounts ?? []; } finally { if (tokenAccounts == null || tokenAccounts.isEmpty) { @@ -419,14 +421,12 @@ class KinServiceImplV4 extends KinService { @override Observer streamAccount(KinAccountId kinAccountId) { - // TODO: implement streamAccount - throw UnimplementedError(); + return streamingApi!.streamAccount(kinAccountId); } @override Observer streamNewTransactions(KinAccountId kinAccountId) { - // TODO: implement streamNewTransactions - throw UnimplementedError(); + return streamingApi!.streamNewTransactions(kinAccountId); } @override @@ -473,4 +473,105 @@ class KinServiceImplV4 extends KinService { }); } + @override + Future> mergeTokenAccounts(KinAccountId accountId, PrivateKey signer, AppIdx appIdx, [bool shouldCreateAssociatedAccount = true]) { + return resolveTokenAccounts(accountId).then((existingAccounts) async { + if(existingAccounts.isEmpty) { + return List.empty(); + } + else { + final serviceConfig; + final cachedRecentBlockhash; + try{ + serviceConfig = await _cachedServiceConfig(); + cachedRecentBlockhash = await _cachedRecentBlockHash(); + } + catch(e){ + throw TransientFailure("Pre-requisite response failed" + e.toString()); + } + + var dest = existingAccounts[0].key; + var instructions = []; + var signers = []; + PublicKey subsidizer = serviceConfig!.payload!.tokenProgram.toKeyPair().asPublicKey(); + final PublicKey owner = signer.asPublicKey(); + final programKey = serviceConfig.payload!.token.toKeyPair().asPublicKey(); + var mint = serviceConfig.payload!.token.toKeyPair().asPublicKey(); + + var memo = null; + if(appIdx.value > 0) { + memo = KinBinaryMemoBuilder(appIdx.value)..setTransferType(TransferType.none)..build(); + } + + var createAssocAccount = + AssociatedTokenProgramCreateAssociatedTokenAccount(subsidizer, owner, mint); + + if(shouldCreateAssociatedAccount) { + if(existingAccounts.isEmpty || existingAccounts[0].key != createAssocAccount.addr) { + if(memo != null) { + instructions.add(MemoProgramBase64EncodedMemo.fromBytes((memo as KinBinaryMemo).encode()).instruction!); + } + + instructions.add(createAssocAccount.instruction!); + instructions.add(SetAuthority( + createAssocAccount.addr, + owner, + subsidizer, + TokenProgramAuthorityTypeCloseAccount(), + programKey + ).instruction!); + + dest = createAssocAccount.addr; + signers.add(signer); + } + } + + for (var tokenAccount in existingAccounts.whereNotNull()) { + if(tokenAccount == dest) { + continue; + } + + instructions.add( + TokenProgramTransfer(tokenAccount.key, + dest, + signer.asPublicKey(), + tokenAccount.balance, TokenProgram.PROGRAM_KEY).instruction! + ); + + signers.add(signer); + if(tokenAccount.closeAuthority == null) { + continue; + } + + for (var account in List.of([null, accountId, (serviceConfig as ServiceConfig).subsidizerAccount.toKeyPair().asPublicKey() + + ])) { + if(tokenAccount.closeAuthority == account) { + instructions.add(CloseAccount( + tokenAccount.key, + tokenAccount.closeAuthority!, + tokenAccount.closeAuthority! + ).instruction!); + } + } + } + + var tx = Transaction.newTransaction( + (serviceConfig as ServiceConfig).subsidizerAccount.toKeyPair().asPublicKey(), + instructions.toList()) + ..copyAndSetRecentBlockhash((cachedRecentBlockhash as Hash)) + ..copyAndSign(signers.toList()); + + var kinTransaction = SolanaKinTransaction(tx.marshal(), null, networkEnvironment, null); + submitTransaction(kinTransaction) + ..whenComplete( + () => + invalidateResolvedTokenAccounts(accountId) + ); + return [KinTokenAccountInfo(createAssocAccount.addr, (existingAccounts.map((e) => e.balance) + ..reduce((acc, kinAmount) => acc + kinAmount).amount).first, null)]; + } + }); + } + } \ No newline at end of file diff --git a/base/lib/base/network/services/kin_service_wrapper.dart b/base/lib/base/network/services/kin_service_wrapper.dart deleted file mode 100644 index 75f86ed..0000000 --- a/base/lib/base/network/services/kin_service_wrapper.dart +++ /dev/null @@ -1 +0,0 @@ -//Placeholder \ No newline at end of file diff --git a/base/lib/base/storage/kin_file_storage.dart b/base/lib/base/storage/kin_file_storage.dart index a3d3bdc..c998323 100644 --- a/base/lib/base/storage/kin_file_storage.dart +++ b/base/lib/base/storage/kin_file_storage.dart @@ -145,11 +145,6 @@ class KinFileStorage implements Storage { return 3 ; } - @override - Future getMinFee() async { - return null; - } - @override String getOrCreateCID() { throw UnimplementedError(); diff --git a/base/lib/base/storage/storage.dart b/base/lib/base/storage/storage.dart index 721b2bb..67fe3b9 100644 --- a/base/lib/base/storage/storage.dart +++ b/base/lib/base/storage/storage.dart @@ -61,10 +61,6 @@ abstract class Storage { Future updateAccountBalance( KinAccountId accountId, KinBalance balance); - Future setMinFee(QuarkAmount minFee); - - Future getMinFee(); - Future deleteAllStorage([KinAccountId? accountId]); Future setMinApiVersion(int apiVersion); diff --git a/base_compat/pubspec.lock b/base_compat/pubspec.lock index 010beab..f9203a9 100644 --- a/base_compat/pubspec.lock +++ b/base_compat/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" boolean_selector: dependency: transitive description: @@ -28,7 +28,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -73,7 +73,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: @@ -127,7 +127,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" typed_data: dependency: transitive description: diff --git a/design/pubspec.lock b/design/pubspec.lock index 010beab..f9203a9 100644 --- a/design/pubspec.lock +++ b/design/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" boolean_selector: dependency: transitive description: @@ -28,7 +28,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -73,7 +73,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: @@ -127,7 +127,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" typed_data: dependency: transitive description: diff --git a/examples/example_console/bin/example_features.dart b/examples/example_console/bin/example_features.dart index 3c584b4..d40ef89 100644 --- a/examples/example_console/bin/example_features.dart +++ b/examples/example_console/bin/example_features.dart @@ -26,7 +26,8 @@ void main(List args) async { appIndex, 'Example App', storageLocation: - '/tmp/kin-flutter-example-${DateTime.now().millisecondsSinceEpoch}', + '/tmp/kin-flutter-feature', + createAccountIfEmpty: true, ); print(kin); diff --git a/spend/pubspec.lock b/spend/pubspec.lock index 010beab..f9203a9 100644 --- a/spend/pubspec.lock +++ b/spend/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" boolean_selector: dependency: transitive description: @@ -28,7 +28,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -73,7 +73,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: @@ -127,7 +127,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" typed_data: dependency: transitive description: