diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml new file mode 100644 index 0000000..fb080e0 --- /dev/null +++ b/.github/workflows/dart.yml @@ -0,0 +1,51 @@ +name: Flutter Package + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.22.2 + + - name: Install dependencies + run: flutter pub get + + # - name: Run tests + # run: flutter test + + - name: Create credentials.json + env: + PUBDEV_TOKEN: ${{ secrets.PUBDEV_TOKEN }} + run: | + # Set the configuration path + CONFIG_PATH="${XDG_CONFIG_HOME:-$HOME/.config}/dart" + + # Ensure the directory exists + mkdir -p "$CONFIG_PATH" + + # Write the credentials to the appropriate location + echo "$PUBDEV_TOKEN" > "$CONFIG_PATH/pub-credentials.json" + + # Verify the file was created correctly + cat "$CONFIG_PATH/pub-credentials.json" + + - name: Publish to pub.dev (dry run) + run: flutter pub publish --dry-run + + - name: Publish to pub.dev + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: flutter pub publish --force diff --git a/.github/workflows/flutter-test.yml b/.github/workflows/flutter-test.yml deleted file mode 100644 index 52ad084..0000000 --- a/.github/workflows/flutter-test.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: flutter-test - -on: - pull_request: - push: - branches: - - dev - - main - -jobs: - main: - runs-on: ubuntu-20.04 - services: - postgres: - image: postgres:15-alpine - env: - POSTGRES_DB: prisma - POSTGRES_USER: prisma - POSTGRES_PASSWORD: prisma - ports: [ '5432:5432' ] - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - redis: - image: redis:7-alpine - ports: ['6379:6379'] - options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 - solana: - image: ghcr.io/kin-labs/kinetic-solana-network:latest - ports: [ '8899:8899', '8900:8900' ] - kinetic: - image: ghcr.io/kin-labs/kinetic:1.0.0-rc.12 - ports: - - 3000:3000 - options: --health-cmd "wget localhost:3000/api/uptime -q -O - > /dev/null 2>&1" --health-interval 10s --health-timeout 5s --health-retries 5 - env: - API_URL: 'http://localhost:3000' - APP_1_FEE_PAYER_SECRET: 'UvfuF3FPqLyvS8xGjSu4AUfdsY5QvLdnin8SKBLAi3UqgbmEWCDshPY3UcxvBgRAqHLzh5Ni1eypLVZArsis6FF' - APP_1_NAME: 'App 1' - AUTH_USERS: 'alice|Kinetic@alice1|Admin,bob|Kinetic@bob1' - AUTH_PASSWORD_ENABLED: true - DATABASE_URL: 'postgresql://prisma:prisma@postgres:5432/prisma?schema=kinetic' - JWT_SECRET: 'KineticJwtSecret!' - NX_CLOUD_DISTRIBUTED_EXECUTION: false - REDIS_URL: 'redis://redis:6379' - SOLANA_LOCAL_ENABLED: true - SOLANA_LOCAL_MINT_KIN: '*MoGaMuJnB3k8zXjBYBnHxHG47vWcW3nyb7bFYvdVzek,5,Kin' - SOLANA_LOCAL_MINT_KIN_AIRDROP_SECRET: 'UvfuF3FPqLyvS8xGjSu4AUfdsY5QvLdnin8SKBLAi3UqgbmEWCDshPY3UcxvBgRAqHLzh5Ni1eypLVZArsis6FF' - SOLANA_LOCAL_MINT_USDC: '*USDzo281m7rjzeZyxevkzL1vr5Cibb9ek3ynyAjXjUM,2,USDC' - SOLANA_LOCAL_MINT_USDC_AIRDROP_SECRET: 'UvfuF3FPqLyvS8xGjSu4AUfdsY5QvLdnin8SKBLAi3UqgbmEWCDshPY3UcxvBgRAqHLzh5Ni1eypLVZArsis6FF' - SOLANA_LOCAL_RPC_ENDPOINT: 'http://solana:8899' - - steps: - - uses: actions/checkout@v1 - - uses: subosito/flutter-action@v1 - - run: flutter test diff --git a/Makefile b/Makefile index a869305..9a7c79b 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ setup: npm install @openapitools/openapi-generator-cli -g generate_api: - npx openapi-generator-cli generate -i https://raw.githubusercontent.com/kin-labs/kinetic/main/api-swagger.json -g dart -o lib/generated + npx openapi-generator-cli generate -i https://raw.githubusercontent.com/kin-labs/kinetic/dev/api-swagger.json -g dart -o lib/generated generate_pre: rm -rf lib/generated diff --git a/lib/generated/lib/api/account_api.dart b/lib/generated/lib/api/account_api.dart index 919c9ae..9ecb5f4 100644 --- a/lib/generated/lib/api/account_api.dart +++ b/lib/generated/lib/api/account_api.dart @@ -132,13 +132,16 @@ class AccountApi { /// /// * [String] accountId (required): /// + /// * [String] mint (required): + /// /// * [Commitment] commitment (required): - Future getAccountInfoWithHttpInfo(String environment, int index, String accountId, Commitment commitment,) async { + Future getAccountInfoWithHttpInfo(String environment, int index, String accountId, String mint, Commitment commitment,) async { // ignore: prefer_const_declarations - final path = r'/api/account/info/{environment}/{index}/{accountId}' + final path = r'/api/account/info/{environment}/{index}/{accountId}/{mint}' .replaceAll('{environment}', environment) .replaceAll('{index}', index.toString()) - .replaceAll('{accountId}', accountId); + .replaceAll('{accountId}', accountId) + .replaceAll('{mint}', mint); // ignore: prefer_final_locals Object? postBody; @@ -173,9 +176,11 @@ class AccountApi { /// /// * [String] accountId (required): /// + /// * [String] mint (required): + /// /// * [Commitment] commitment (required): - Future getAccountInfo(String environment, int index, String accountId, Commitment commitment,) async { - final response = await getAccountInfoWithHttpInfo(environment, index, accountId, commitment,); + Future getAccountInfo(String environment, int index, String accountId, String mint, Commitment commitment,) async { + final response = await getAccountInfoWithHttpInfo(environment, index, accountId, mint, commitment,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/lib/helpers/generate_make_transfer_transaction.dart b/lib/helpers/generate_make_transfer_transaction.dart index 6e549dc..e907b48 100644 --- a/lib/helpers/generate_make_transfer_transaction.dart +++ b/lib/helpers/generate_make_transfer_transaction.dart @@ -6,52 +6,52 @@ import 'package:kinetic_sdk/tools.dart'; import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; -Future generateMakeTransferTransaction(GenerateMakeTransferOptions options, {List fk = const []}) async { +Future generateMakeTransferTransaction(GenerateMakeTransferOptions options) async { // Create objects from Response + final destinationPublicKey = getPublicKey(options.destination); + final destinationTokenAccountPublicKey = getPublicKey(options.destinationTokenAccount); final feePayerKey = getPublicKey(options.mintFeePayer); final mintKey = getPublicKey(options.mintPublicKey); - - final destinationPublicKey = getPublicKey(options.destination); final ownerPublicKey = options.owner.solanaPublicKey; - - // Get TokenAccount from Owner and Destination - final destinationTokenAccount = await findAssociatedTokenAddress(mint: mintKey, owner: destinationPublicKey); - final ownerTokenAccount = await findAssociatedTokenAddress(mint: mintKey, owner: ownerPublicKey); + final ownerTokenAccountPublicKey = getPublicKey(options.ownerTokenAccount); // Create Instructions List instructions = []; + // Create the Memo Instruction if (options.addMemo) { var memo = createKinMemoInstruction(options.type, options.index); instructions.add(MemoInstruction(signers: [], memo: base64Encode(memo))); } + // Create the Token Account if senderCreate is enabled if (options.senderCreate != null && options.senderCreate!) { instructions.add(AssociatedTokenAccountInstruction.createAccount( - address: destinationTokenAccount, + address: destinationTokenAccountPublicKey, funder: feePayerKey, mint: mintKey, owner: destinationPublicKey, )); } - List signersPublic = [ownerPublicKey, feePayerKey]; - + // Create the Token Transfer Instruction instructions.add(TokenInstruction.transferChecked( decimals: options.mintDecimals, mint: mintKey, - source: ownerTokenAccount, - destination: destinationTokenAccount, + source: ownerTokenAccountPublicKey, + destination: destinationTokenAccountPublicKey, owner: ownerPublicKey, amount: getRawQuantity(double.parse(options.amount), options.mintDecimals).toInt(), - signers: signersPublic, + signers: [ownerPublicKey, feePayerKey], )); + // Create transaction final CompiledMessage message = Message(instructions: instructions).compile( recentBlockhash: options.blockhash, feePayer: feePayerKey, ); + // Partially sign the transaction return SignedTx( signatures: [ Signature(List.filled(64, 0), publicKey: feePayerKey), diff --git a/lib/helpers/get_token_address.dart b/lib/helpers/get_token_address.dart new file mode 100644 index 0000000..ca0b8a1 --- /dev/null +++ b/lib/helpers/get_token_address.dart @@ -0,0 +1,12 @@ +import 'package:kinetic/helpers/get_public_key.dart'; +import 'package:solana/solana.dart'; + +// Function with 2 params +Future getTokenAddress({ + required String account, + required String mint, +}) async { + final address = await findAssociatedTokenAddress(mint: getPublicKey(mint), owner: getPublicKey(account)); + + return address.toString(); +} diff --git a/lib/interfaces/generate_make_transfer_options.dart b/lib/interfaces/generate_make_transfer_options.dart index dae5e85..ca89a09 100644 --- a/lib/interfaces/generate_make_transfer_options.dart +++ b/lib/interfaces/generate_make_transfer_options.dart @@ -7,12 +7,14 @@ class GenerateMakeTransferOptions { required this.amount, required this.blockhash, required this.destination, + required this.destinationTokenAccount, required this.index, required this.lastValidBlockHeight, required this.mintDecimals, required this.mintFeePayer, required this.mintPublicKey, required this.owner, + required this.ownerTokenAccount, required this.type, this.senderCreate, }); @@ -21,12 +23,14 @@ class GenerateMakeTransferOptions { late String amount; late String blockhash; late String destination; + late String destinationTokenAccount; late int index; late int lastValidBlockHeight; late int mintDecimals; late String mintFeePayer; late String mintPublicKey; late Keypair owner; + late String ownerTokenAccount; late bool? senderCreate; late TransactionType type; } diff --git a/lib/interfaces/get_account_info_options.dart b/lib/interfaces/get_account_info_options.dart index 438508d..5666906 100644 --- a/lib/interfaces/get_account_info_options.dart +++ b/lib/interfaces/get_account_info_options.dart @@ -4,8 +4,10 @@ class GetAccountInfoOptions { GetAccountInfoOptions({ required this.account, this.commitment, + this.mint, }); late String account; late Commitment? commitment; + late String? mint; } diff --git a/lib/interfaces/prepare_transaction_response.dart b/lib/interfaces/prepare_transaction_response.dart new file mode 100644 index 0000000..75c9218 --- /dev/null +++ b/lib/interfaces/prepare_transaction_response.dart @@ -0,0 +1,5 @@ +class PrepareTransactionResponse { + final String blockhash; + final int lastValidBlockHeight; + PrepareTransactionResponse({required this.blockhash, required this.lastValidBlockHeight}); +} diff --git a/lib/kinetic_sdk_internal.dart b/lib/kinetic_sdk_internal.dart index 162bf35..f39345f 100644 --- a/lib/kinetic_sdk_internal.dart +++ b/lib/kinetic_sdk_internal.dart @@ -99,12 +99,15 @@ class KineticSdkInternal { } Future getAccountInfo(GetAccountInfoOptions options) async { + var appConfig = _ensureAppConfig(); Commitment commitment = _getCommitment(options.commitment); + AppConfigMint? mint = _getAppMint(appConfig, options.mint); return accountApi.getAccountInfo( sdkConfig.environment, sdkConfig.index, options.account, + mint.publicKey, commitment, ); } @@ -163,21 +166,45 @@ class KineticSdkInternal { Future makeTransfer(MakeTransferOptions options) async { var appConfig = _ensureAppConfig(); + Commitment commitment = _getCommitment(options.commitment); AppConfigMint? mint = _getAppMint(appConfig, options.mint); - var commitment = options.commitment ?? Commitment.confirmed; var destination = options.destination; var senderCreate = options.senderCreate ?? false; - _validateDestination(appConfig, destination); + // We get the token account for the owner + var ownerTokenAccount = await _findTokenAccount( + account: options.owner.publicKey, + commitment: commitment, + mint: mint.publicKey, + ); + + // The operation fails if the owner doesn't have a token account for this mint + if (ownerTokenAccount == null) { + throw Exception("Owner account doesn't exist for mint ${mint.publicKey}."); + } - List? accounts = await getTokenAccounts(GetTokenAccountsOptions( - account: options.destination, + // We get the account info for the destination + var destinationTokenAccount = await _findTokenAccount( + account: destination, + commitment: commitment, mint: mint.publicKey, - )); + ); - if (!senderCreate && (accounts == null || accounts.isEmpty)) { - throw Exception("Destination account does not exist"); + // The operation fails if the destination doesn't have a token account for this mint and senderCreate is not set + if (destinationTokenAccount == null && !senderCreate) { + throw Exception("Destination account doesn't exist for mint ${mint.publicKey}."); + } + + // Derive the associated token address if the destination doesn't have a token account for this mint and senderCreate is set + String? senderCreateTokenAccount; + if (destinationTokenAccount == null && senderCreate) { + senderCreateTokenAccount = await getTokenAddress(account: destination, mint: mint.publicKey); + } + + // The operation fails if there is still no destination token account + if (destinationTokenAccount == null && senderCreateTokenAccount == null) { + throw Exception("Destination account not found."); } PrepareTransactionResponse blockhash = await _getBlockhash(); @@ -187,12 +214,14 @@ class KineticSdkInternal { amount: options.amount, blockhash: blockhash.blockhash, destination: options.destination, + destinationTokenAccount: (destinationTokenAccount ?? senderCreateTokenAccount)!, index: sdkConfig.index, lastValidBlockHeight: blockhash.lastValidBlockHeight, mintDecimals: mint.decimals, mintFeePayer: mint.feePayer, mintPublicKey: mint.publicKey, owner: options.owner, + ownerTokenAccount: ownerTokenAccount, senderCreate: options.senderCreate, type: options.type ?? TransactionType.none, )); @@ -233,6 +262,23 @@ class KineticSdkInternal { return appConfig!; } + Future _findTokenAccount( + {required String account, required Commitment commitment, required String mint}) async { + // We get the account info for the account + var accountInfo = await getAccountInfo(GetAccountInfoOptions( + account: account, + commitment: commitment, + mint: mint, + )); + // The operation fails when the account is a mint account + if (accountInfo != null && accountInfo.isMint) { + throw Exception("Account is a mint account."); + } + // Find the token account for this mint + // FIXME: we need to support the use case where the account has multiple accounts for this mint + return accountInfo?.tokens?.firstWhere((element) => element.mint == mint).account; + } + AppConfigMint _getAppMint(AppConfig appConfig, String? mint) { mint = mint ?? appConfig.mint.publicKey; final AppConfigMint? found = appConfig.mints.firstWhere((element) => element.publicKey == mint); @@ -265,9 +311,3 @@ class KineticSdkInternal { } } } - -class PrepareTransactionResponse { - final String blockhash; - final int lastValidBlockHeight; - PrepareTransactionResponse({required this.blockhash, required this.lastValidBlockHeight}); -} diff --git a/pubspec.yaml b/pubspec.yaml index 9373999..bb54203 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,9 +1,9 @@ name: kinetic_sdk version: 1.0.0-rc.13 -description: The official Kinetic Dart SDK brought to you by the Kin Foundation. -homepage: https://github.com/kinnytips/kinetic-dart -repository: https://github.com/kinnytips/kinetic-dart -issue_tracker: https://github.com/kinnytips/kinetic-dart/issues +description: The official Kinetic SDK +homepage: https://github.com/kinnytips/kinetic-dart-sdk +repository: https://github.com/kinnytips/kinetic-dart-sdk +issue_tracker: https://github.com/kinnytips/kinetic-dart-sdk/issues documentation: https://kinny.io/docs/developers/flutter-dart environment: