diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 000000000..7eede62c5 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,220 @@ +name: Mobile CD +on: + # Allows you to run this workflow manually from the Actions a tab + workflow_dispatch: + +jobs: + ios-testflight-build: + name: ios-testflight-build + strategy: + matrix: + os: [macos-13] + node-version: [18.18.0] + ruby-version: [3.2] + xcode: [15.0.1] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout to git repository + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v3 + with: + cache: 'yarn' + node-version: ${{ matrix.node-version }} + + - name: Install yarn dependencies + uses: nick-fields/retry@v2 + with: + timeout_minutes: 10 + max_attempts: 3 + command: yarn + + - name: Install pods dependencies + run: yarn pods + + - name: Set up Ruby and Gemfile dependencies + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + working-directory: './packages/mobile' + + - name: Decode signing certificate into a file + working-directory: './packages/mobile/ios' + env: + CERTIFICATE_BASE64: ${{ secrets.IOS_DIST_SIGNING_KEY }} + run: | + echo $CERTIFICATE_BASE64 | base64 --decode > signing-cert.p12 + + - name: Build & upload iOS binary + working-directory: './packages/mobile/ios' + run: bundle exec fastlane ios beta + env: + X_CODE: ${{ matrix.xcode }} + DEVELOPER_APP_IDENTIFIER: ${{ secrets.DEVELOPER_APP_IDENTIFIER }} + DEVELOPER_TEAM_ID: ${{ secrets.DEVELOPER_TEAM_ID }} + ASC_KEY_ID: ${{ secrets.APPLE_KEY_ID }} + ASC_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} + ASC_KEY: ${{ secrets.APPLE_KEY_CONTENT }} + SIGNING_KEY_PASSWORD: ${{ secrets.IOS_DIST_SIGNING_KEY_PASSWORD }} + SIGNING_KEY_FILE_PATH: signing-cert.p12 + + - name: Upload logs to artifacts + uses: actions/upload-artifact@v3 + if: failure() + with: + name: gum-logs + path: /Users/runner/Library/Logs/gym/ton_keeper-ton_keeper.log + + - name: Upload app-store ipa and dsyms to artifacts + uses: actions/upload-artifact@v3 + with: + name: Tonkeeper ipa & dsyms ${{ env.VERSION_CODE }} + path: | + ./packages/mobile/ios/ton_keeper.ipa + ./packages/mobile/ios/*.app.dSYM.zip + + android-store-build: + name: android-store-build + strategy: + matrix: + os: [macos-13] + node-version: [18.18.0] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout to git repository + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v3 + with: + cache: 'yarn' + node-version: ${{ matrix.node-version }} + + - name: Install yarn dependencies + uses: nick-fields/retry@v2 + with: + timeout_minutes: 10 + max_attempts: 3 + command: yarn + + - name: Set up Ruby and Gemfile dependencies + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + working-directory: './packages/mobile' + + - name: Update Java home to 11 version + run: export JAVA_HOME=$JAVA_HOME_11_X64 + + - name: Decode signing certificate into a file + working-directory: './packages/mobile/android/app' + env: + CERTIFICATE_BASE64: ${{ secrets.ANDROID_DIST_SIGNING_KEY }} + run: | + echo $CERTIFICATE_BASE64 | base64 --decode > google-release.keystore + + - name: Decode service account into a file + working-directory: './packages/mobile/android' + env: + CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }} + run: | + echo $CREDENTIALS > service-account.json + + - name: Patch for Google Play (remove REQUEST_INSTALL_PACKAGES) + run: | + git apply ./patches/google-play-release.patch + + - name: Build & deploy Android release + working-directory: './packages/mobile/android' + run: bundle exec fastlane android beta + env: + KEYSTORE_FILE: ${{ github.workspace }}/packages/mobile/android/app/google-release.keystore + KEYSTORE_PASSWORD: ${{ secrets.TONKEEPER_UPLOAD_STORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.TONKEEPER_UPLOAD_KEY_ALIAS}} + KEY_PASSWORD: ${{ secrets.TONKEEPER_UPLOAD_KEY_PASSWORD }} + ANDROID_JSON_KEY_FILE: service-account.json + + - name: Upload android google play release to artifacts + uses: actions/upload-artifact@v3 + with: + name: Tonkeeper aab ${{ env.VERSION_CODE }} + path: | + ${{ github.workspace }}/packages/mobile/android/app/build/outputs + + android-site-build: + name: android-site-build + strategy: + matrix: + os: [macos-13] + node-version: [18.18.0] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout to git repository + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v3 + with: + cache: 'yarn' + node-version: ${{ matrix.node-version }} + + - name: Install yarn dependencies + uses: nick-fields/retry@v2 + with: + timeout_minutes: 10 + max_attempts: 3 + command: yarn + + - name: Set up Ruby and Gemfile dependencies + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + working-directory: './packages/mobile' + + - name: Update Java home to 11 version + run: export JAVA_HOME=$JAVA_HOME_11_X64 + + - name: Update ENVFILE for site + working-directory: './packages/mobile' + run: cp .env.site .env + + - name: Decode signing certificate into a file + working-directory: './packages/mobile/android/app' + env: + CERTIFICATE_BASE64: ${{ secrets.ANDROID_DIST_SIGNING_KEY }} + run: | + echo $CERTIFICATE_BASE64 | base64 --decode > google-release.keystore + + - name: Decode service account into a file + working-directory: './packages/mobile/android' + env: + CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }} + run: | + echo $CREDENTIALS > service-account.json + + - name: Build android apk + working-directory: './packages/mobile/android' + run: bundle exec fastlane android apk + env: + KEYSTORE_FILE: ${{ github.workspace }}/packages/mobile/android/app/google-release.keystore + KEYSTORE_PASSWORD: ${{ secrets.TONKEEPER_UPLOAD_STORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.TONKEEPER_UPLOAD_KEY_ALIAS}} + KEY_PASSWORD: ${{ secrets.TONKEEPER_UPLOAD_KEY_PASSWORD }} + ANDROID_JSON_KEY_FILE: service-account.json + + - name: Upload android apk to artifacts + uses: actions/upload-artifact@v3 + with: + name: Tonkeeper apk ${{ env.VERSION_CODE }} + path: | + ${{ github.workspace }}/packages/mobile/android/app/build/outputs \ No newline at end of file diff --git a/.gitignore b/.gitignore index b1ae71625..52c29e35d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ # .DS_Store .env +signing-cert.p12 +*.dSYM.zip bridge.html diff --git a/packages/@core-js/package.json b/packages/@core-js/package.json index 0178b90b8..d21b70ab8 100644 --- a/packages/@core-js/package.json +++ b/packages/@core-js/package.json @@ -15,13 +15,13 @@ "@aws-crypto/sha256-js": "^3.0.0", "@ethersproject/shims": "^5.7.0", "@noble/ed25519": "1.7.3", + "@ton/core": "^0.53.0", + "@ton/crypto": "^3.2.0", + "@ton/ton": "^13.9.0", "aes-js": "3.1.2", "bignumber.js": "^9.1.1", "ethers": "^6.7.1", "isomorphic-webcrypto": "^2.3.8", - "nanoid": "^5.0.1", - "ton": "^13.5.0", - "ton-core": "^0.50.0", - "ton-crypto": "^3.2.0" + "nanoid": "^5.0.1" } } diff --git a/packages/@core-js/src/formatters/Address.ts b/packages/@core-js/src/formatters/Address.ts index 8d8632a1b..47f2fd03a 100644 --- a/packages/@core-js/src/formatters/Address.ts +++ b/packages/@core-js/src/formatters/Address.ts @@ -59,6 +59,11 @@ export class Address { return TonWeb.Address.isValid(address); } + static isBounceable(address: string) { + const addr = new TonWeb.Address(address); + return !addr.isUserFriendly || addr.isBounceable; + } + static compare(adr1?: string, adr2?: string) { if (adr1 === undefined || adr2 === undefined) { return false; diff --git a/packages/@core-js/src/index.ts b/packages/@core-js/src/index.ts index cd8d2b558..75a296256 100644 --- a/packages/@core-js/src/index.ts +++ b/packages/@core-js/src/index.ts @@ -5,6 +5,7 @@ export * from './formatters/DNS'; export * from './utils/AmountFormatter/FiatCurrencyConfig'; export * from './utils/AmountFormatter'; export * from './utils/network'; +export * from './utils/tonapiUtils'; export * from './useWallet'; export * from './Tonkeeper'; diff --git a/packages/@core-js/src/legacy/contracts/LockupContractV1.ts b/packages/@core-js/src/legacy/contracts/LockupContractV1.ts new file mode 100644 index 000000000..7d259ce2a --- /dev/null +++ b/packages/@core-js/src/legacy/contracts/LockupContractV1.ts @@ -0,0 +1,178 @@ +import { + Address, + beginCell, + Cell, + Contract, + contractAddress, + ContractProvider, + internal, + MessageRelaxed, + Sender, + SendMode, +} from '@ton/core'; +import { Maybe } from '@ton/ton/dist/utils/maybe'; +import { createWalletTransferV3 } from '@ton/ton/dist/wallets/signing/createWalletTransfer'; + +export interface LockupContractV1AdditionalParams { + allowedDestinations?: Maybe; + lockupPubKey?: Maybe; +} + +export class LockupContractV1 implements Contract { + static create(args: { + workchain: number; + publicKey: Buffer; + walletId?: Maybe; + additionalParams?: LockupContractV1AdditionalParams; + }) { + return new LockupContractV1( + args.workchain, + args.publicKey, + args.additionalParams?.allowedDestinations, + args.additionalParams?.lockupPubKey ?? '', + ); + } + + readonly workchain: number; + readonly publicKey: Buffer; + readonly address: Address; + readonly walletId: number; + readonly init: { data: Cell; code: Cell }; + + private constructor( + workchain: number, + publicKey: Buffer, + allowedDestinations: Maybe, + configPubKey: string, + walletId?: Maybe, + ) { + // Resolve parameters + this.workchain = workchain; + this.publicKey = publicKey; + if (walletId !== null && walletId !== undefined) { + this.walletId = walletId; + } else { + this.walletId = 698983191 + workchain; + } + // Build initial code and data + let code = Cell.fromBoc( + Buffer.from( + 'te6ccsECHgEAAmEAAAAADQASABcAHAAhACYApwCvALwAxgDSAOsA8AD1ARIBNgFbAWABZQFqAYMBiAGWAaQBqQG0AcIBzwJLART/APSkE/S88sgLAQIBIAIcAgFIAxECAs0EDAIBIAULAgEgBgoD9wB0NMDAXGwkl8D4PpAMCHHAJJfA+AB0x8hwQKSXwTg8ANRtPABghCC6vnEUrC9sJJfDOCAKIIQgur5xBu6GvL0gCErghA7msoAvvL0B4MI1xiAICH5AVQQNvkQEvL00x+AKYIQNzqp9BO6EvL00wDTHzAB4w8QSBA3XjKAHCAkADBA5SArwBQAWEDdBCvAFCBBXUFYAEBAkQwDwBO1UABMIddJ9KhvpWwxgAC1e1E0NMf0x/T/9P/9AT6APQE+gD0BNGAIBIA0QAgEgDg8ANQIyMofF8ofFcv/E8v/9AAB+gL0AAH6AvQAyYABDFEioFMTgCD0Dm+hlvoA0ROgApEw4shQA/oCQBOAIPRDAYABFSOHiKAIPSWb6UgkzAju5Ex4iCYNfoA0ROhQBOSbCHis+YwgCASASGwIBIBMYAgEgFBUALbUYfgBtiIaKgmCeAMYgfgDGPwTt4gswAgFYFhcAF63OdqJoaZ+Y64X/wAAXrHj2omhpj5jrhY/AAgFIGRoAEbMl+1E0NcLH4AAXsdG+COCAQjD7UPYgABW96feAGIJC+EeADAHy8oMI1xgg0x/TH9MfgCQD+CO7E/Ly8AOAIlGpuhry9IAjUbe6G/L0gB8L+QFUEMX5EBry9PgAUFf4I/AGUJj4I/AGIHEokyDXSo6L0wcx1FEb2zwSsAHoMJIpoN9y+wIGkyDXSpbTB9QC+wDo0QOkR2gUFUMw8ATtVB0AKAHQ0wMBeLCSW3/g+kAx+kAwAfAB2Ae6sw==', + 'base64', + ), + )[0]; + let data = beginCell() + .storeUint(0, 32) // Seqno + .storeUint(this.walletId, 32) + .storeBuffer(publicKey) + .storeBuffer(Buffer.from(configPubKey, 'base64')); + if (allowedDestinations) { + data = data.storeBit(1); + data = data.storeRef(Cell.fromBoc(Buffer.from(allowedDestinations, 'base64'))[0]); + } else { + data = data.storeBit(0); + } + + data = data.storeCoins(0).storeBit(0).storeCoins(0).storeBit(0); + + let cell = data.endCell(); + + this.init = { code, data: cell }; + this.address = contractAddress(workchain, { code, data: cell }); + } + + /** + * Get Wallet Balance + */ + async getBalance(provider: ContractProvider) { + let state = await provider.getState(); + return state.balance; + } + + /** + * Get Wallet Seqno + */ + async getSeqno(provider: ContractProvider) { + let state = await provider.getState(); + if (state.state.type === 'active') { + let res = await provider.get('seqno', []); + return res.stack.readNumber(); + } else { + return 0; + } + } + + /** + * Send signed transfer + */ + async send(provider: ContractProvider, message: Cell) { + await provider.external(message); + } + + /** + * Sign and send transfer + */ + async sendTransfer( + provider: ContractProvider, + args: { + seqno: number; + secretKey: Buffer; + messages: MessageRelaxed[]; + sendMode?: Maybe; + timeout?: Maybe; + }, + ) { + let transfer = this.createTransfer(args); + await this.send(provider, transfer); + } + + /** + * Create signed transfer + */ + createTransfer(args: { + seqno: number; + secretKey: Buffer; + messages: MessageRelaxed[]; + sendMode?: Maybe; + timeout?: Maybe; + }) { + let sendMode = SendMode.PAY_GAS_SEPARATELY; + if (args.sendMode !== null && args.sendMode !== undefined) { + sendMode = args.sendMode; + } + return createWalletTransferV3({ + seqno: args.seqno, + sendMode, + secretKey: args.secretKey, + messages: args.messages, + timeout: args.timeout, + walletId: this.walletId, + }); + } + + /** + * Create sender + */ + sender(provider: ContractProvider, secretKey: Buffer): Sender { + return { + send: async (args) => { + let seqno = await this.getSeqno(provider); + let transfer = this.createTransfer({ + seqno, + secretKey, + sendMode: args.sendMode, + messages: [ + internal({ + to: args.to, + value: args.value, + init: args.init, + body: args.body, + bounce: args.bounce, + }), + ], + }); + await this.send(provider, transfer); + }, + }; + } +} diff --git a/packages/@core-js/src/legacy/contracts/WalletContractV4R1.ts b/packages/@core-js/src/legacy/contracts/WalletContractV4R1.ts new file mode 100644 index 000000000..d0a4d648b --- /dev/null +++ b/packages/@core-js/src/legacy/contracts/WalletContractV4R1.ts @@ -0,0 +1,151 @@ +import { + Address, + beginCell, + Cell, + Contract, + contractAddress, + ContractProvider, + internal, + MessageRelaxed, + Sender, + SendMode, +} from '@ton/core'; +import { Maybe } from '@ton/ton/dist/utils/maybe'; +import { createWalletTransferV4 } from '@ton/ton/dist/wallets/signing/createWalletTransfer'; + +export class WalletContractV4R1 implements Contract { + static create(args: { + workchain: number; + publicKey: Buffer; + walletId?: Maybe; + }) { + return new WalletContractV4R1(args.workchain, args.publicKey, args.walletId); + } + + readonly workchain: number; + readonly publicKey: Buffer; + readonly address: Address; + readonly walletId: number; + readonly init: { data: Cell; code: Cell }; + + private constructor(workchain: number, publicKey: Buffer, walletId?: Maybe) { + // Resolve parameters + this.workchain = workchain; + this.publicKey = publicKey; + if (walletId !== null && walletId !== undefined) { + this.walletId = walletId; + } else { + this.walletId = 698983191 + workchain; + } + // Build initial code and data + let code = Cell.fromBoc( + Buffer.from( + 'te6cckECFQEAAvUAART/APSkE/S88sgLAQIBIAIQAgFIAwcD7tAB0NMDAXGwkVvgIddJwSCRW+AB0x8hghBwbHVnvSKCEGJsbmO9sCKCEGRzdHK9sJJfA+AC+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwXgBNM/yCWCEHBsdWe6kTHjDSSCEGJsbmO64wAEBAUGAFAB+gD0BDCCEHBsdWeDHrFwgBhQBcsFJ88WUAP6AvQAEstpyx9SEMs/AFL4J28ighBibG5jgx6xcIAYUAXLBSfPFiT6AhTLahPLH1Iwyz8B+gL0AACSghBkc3Ryuo41BIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UghBkc3Rygx6xcIAYUATLBVjPFiL6AhLLassfyz+UEDRfBOLJgED7AAIBIAgPAgEgCQ4CAVgKCwA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIAwNABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AABG4yX7UTQ1wsfgAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAT48oMI1xgg0x/TH9MfAvgju/Jj7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/xESExQAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJcfsAyEAUgQEI9FHypwIAbIEBCNcYyFQgJYEBCPRR8qeCEG5vdGVwdIAYyMsFywJQBM8WghAF9eEA+gITy2oSyx/JcfsAAgBygQEI1xgwUgKBAQj0WfKn+CWCEGRzdHJwdIAYyMsFywJQBc8WghAF9eEA+gIUy2oTyx8Syz/Jc/sAAAr0AMntVHbNOpo=', + 'base64', + ), + )[0]; + let data = beginCell() + .storeUint(0, 32) // Seqno + .storeUint(this.walletId, 32) + .storeBuffer(this.publicKey) + .storeBit(0) // Empty plugins dict + .endCell(); + this.init = { code, data }; + this.address = contractAddress(workchain, { code, data }); + } + + /** + * Get Wallet Balance + */ + async getBalance(provider: ContractProvider) { + let state = await provider.getState(); + return state.balance; + } + + /** + * Get Wallet Seqno + */ + async getSeqno(provider: ContractProvider) { + let state = await provider.getState(); + if (state.state.type === 'active') { + let res = await provider.get('seqno', []); + return res.stack.readNumber(); + } else { + return 0; + } + } + + /** + * Send signed transfer + */ + async send(provider: ContractProvider, message: Cell) { + await provider.external(message); + } + + /** + * Sign and send transfer + */ + async sendTransfer( + provider: ContractProvider, + args: { + seqno: number; + secretKey: Buffer; + messages: MessageRelaxed[]; + sendMode?: Maybe; + timeout?: Maybe; + }, + ) { + let transfer = this.createTransfer(args); + await this.send(provider, transfer); + } + + /** + * Create signed transfer + */ + createTransfer(args: { + seqno: number; + secretKey: Buffer; + messages: MessageRelaxed[]; + sendMode?: Maybe; + timeout?: Maybe; + }) { + let sendMode = SendMode.PAY_GAS_SEPARATELY; + if (args.sendMode !== null && args.sendMode !== undefined) { + sendMode = args.sendMode; + } + return createWalletTransferV4({ + seqno: args.seqno, + sendMode, + secretKey: args.secretKey, + messages: args.messages, + timeout: args.timeout, + walletId: this.walletId, + }); + } + + /** + * Create sender + */ + sender(provider: ContractProvider, secretKey: Buffer): Sender { + return { + send: async (args) => { + let seqno = await this.getSeqno(provider); + let transfer = this.createTransfer({ + seqno, + secretKey, + sendMode: args.sendMode, + messages: [ + internal({ + to: args.to, + value: args.value, + init: args.init, + body: args.body, + bounce: args.bounce, + }), + ], + }); + await this.send(provider, transfer); + }, + }; + } +} diff --git a/packages/@core-js/src/legacy/contracts/index.ts b/packages/@core-js/src/legacy/contracts/index.ts new file mode 100644 index 000000000..f538729ac --- /dev/null +++ b/packages/@core-js/src/legacy/contracts/index.ts @@ -0,0 +1,2 @@ +export * from './LockupContractV1'; +export * from './WalletContractV4R1'; diff --git a/packages/@core-js/src/legacy/index.ts b/packages/@core-js/src/legacy/index.ts index ebd852b44..f96d580d0 100644 --- a/packages/@core-js/src/legacy/index.ts +++ b/packages/@core-js/src/legacy/index.ts @@ -1 +1,2 @@ -export * from './tonApiV2'; \ No newline at end of file +export * from './tonApiV2'; +export * from './contracts'; diff --git a/packages/@core-js/src/service/contractService.ts b/packages/@core-js/src/service/contractService.ts new file mode 100644 index 000000000..bd3332f69 --- /dev/null +++ b/packages/@core-js/src/service/contractService.ts @@ -0,0 +1,105 @@ +import { AnyAddress, tonAddress } from './transactionService'; +import { beginCell, Cell, comment } from '@ton/core'; +import { + WalletContractV4R1, + LockupContractV1, + LockupContractV1AdditionalParams, +} from '../legacy'; +import { WalletContractV3R1, WalletContractV3R2, WalletContractV4 } from '@ton/ton'; + +export enum WalletVersion { + v3R1 = 0, + v3R2 = 1, + v4R1 = 2, + v4R2 = 3, + LockupV1 = 4, +} + +export const contractVersionsMap = { + v4R2: WalletVersion.v4R2, + v4R1: WalletVersion.v4R1, + v3R2: WalletVersion.v3R2, + v3R1: WalletVersion.v3R1, + 'lockup-0.1': WalletVersion.LockupV1, +}; + +export type WalletContract = + | LockupContractV1 + | WalletContractV3R1 + | WalletContractV3R2 + | WalletContractV4R1 + | WalletContractV4; + +export interface CreateNftTransferBodyParams { + forwardAmount?: number | bigint; + /* Address for return excesses */ + excessesAddress: AnyAddress; + /* Address of new owner's address */ + newOwnerAddress: AnyAddress; + forwardBody?: Cell | string; + queryId?: number; +} + +export interface CreateJettonTransferBodyParams { + forwardAmount?: number | bigint; + /* Address for return excesses */ + excessesAddress: AnyAddress; + receiverAddress: AnyAddress; + jettonAmount: number | bigint; + forwardBody?: Cell | string; + queryId?: number; +} + +const workchain = 0; +export class ContractService { + static getWalletContract( + version: WalletVersion, + publicKey: Buffer, + additionalParams?: LockupContractV1AdditionalParams, + ) { + switch (version) { + case WalletVersion.v3R1: + return WalletContractV3R1.create({ workchain, publicKey }); + case WalletVersion.v3R2: + return WalletContractV3R2.create({ workchain, publicKey }); + case WalletVersion.v4R1: + return WalletContractV4R1.create({ workchain, publicKey }); + case WalletVersion.v4R2: + return WalletContractV4.create({ workchain, publicKey }); + case WalletVersion.LockupV1: + return LockupContractV1.create({ workchain, publicKey, additionalParams }); + } + } + + static prepareForwardBody(body?: Cell | string) { + return typeof body === 'string' ? comment(body) : body; + } + + static createNftTransferBody(createNftTransferBodyParams: CreateNftTransferBodyParams) { + return beginCell() + .storeUint(0x5fcc3d14, 32) + .storeUint(createNftTransferBodyParams.queryId || 0, 64) + .storeAddress(tonAddress(createNftTransferBodyParams.newOwnerAddress)) + .storeAddress(tonAddress(createNftTransferBodyParams.excessesAddress)) + .storeBit(false) + .storeCoins(createNftTransferBodyParams.forwardAmount ?? 1n) + .storeMaybeRef(this.prepareForwardBody(createNftTransferBodyParams.forwardBody)) + .endCell(); + } + + static createJettonTransferBody( + createJettonTransferBodyParams: CreateJettonTransferBodyParams, + ) { + return beginCell() + .storeUint(0xf8a7ea5, 32) // request_transfer op + .storeUint(createJettonTransferBodyParams.queryId || 0, 64) + .storeCoins(createJettonTransferBodyParams.jettonAmount) + .storeAddress(tonAddress(createJettonTransferBodyParams.receiverAddress)) + .storeAddress(tonAddress(createJettonTransferBodyParams.excessesAddress)) + .storeBit(false) // null custom_payload + .storeCoins(createJettonTransferBodyParams.forwardAmount ?? 1n) + .storeBit(createJettonTransferBodyParams.forwardBody != null) // forward_payload in this slice - false, separate cell - true + .storeMaybeRef(this.prepareForwardBody(createJettonTransferBodyParams.forwardBody)) + .endCell(); + } +} diff --git a/packages/@core-js/src/service/cryptoService.ts b/packages/@core-js/src/service/cryptoService.ts index 5fdf70055..e6671857b 100644 --- a/packages/@core-js/src/service/cryptoService.ts +++ b/packages/@core-js/src/service/cryptoService.ts @@ -7,7 +7,7 @@ */ import { Sha256 } from '@aws-crypto/sha256-js'; -import { Address, Cell, beginCell } from 'ton-core'; +import { Address, Cell, beginCell } from '@ton/core'; import * as ed25519 from '@noble/ed25519'; import aesjs from 'aes-js'; import crypto from 'isomorphic-webcrypto'; diff --git a/packages/@core-js/src/service/index.ts b/packages/@core-js/src/service/index.ts index e9d21598a..befba8347 100644 --- a/packages/@core-js/src/service/index.ts +++ b/packages/@core-js/src/service/index.ts @@ -1 +1,3 @@ export * from './cryptoService'; +export * from './transactionService'; +export * from './contractService'; diff --git a/packages/@core-js/src/service/transactionService.ts b/packages/@core-js/src/service/transactionService.ts new file mode 100644 index 000000000..c03ed1616 --- /dev/null +++ b/packages/@core-js/src/service/transactionService.ts @@ -0,0 +1,100 @@ +import { + Address, + beginCell, + Cell, + external, + internal, + storeMessage, + MessageRelaxed, + SendMode, + loadStateInit, +} from '@ton/core'; +import { Address as AddressFormatter } from '../formatters/Address'; +import { WalletContract } from './contractService'; +import { SignRawMessage } from '@tonkeeper/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types'; + +export type AnyAddress = string | Address | AddressFormatter; + +export interface TransferParams { + seqno: number; + sendMode?: number; + secretKey: Buffer; + messages: MessageRelaxed[]; +} + +export function tonAddress(address: AnyAddress) { + if (typeof address === 'string') { + return Address.parse(address); + } + if (address instanceof AddressFormatter) { + return Address.parse(address.toRaw()); + } + return address; +} + +export class TransactionService { + private static TTL = 5 * 60; + + private static getTimeout() { + return Math.floor(Date.now() / 1e3) + TransactionService.TTL; + } + + private static externalMessage(contract: WalletContract, seqno: number, body: Cell) { + return beginCell() + .storeWritable( + storeMessage( + external({ + to: contract.address, + init: seqno === 0 ? contract.init : undefined, + body: body, + }), + ), + ) + .endCell(); + } + + private static getBounceFlagFromAddress(address: string) { + try { + return Address.isFriendly(address) + ? Address.parseFriendly(address).isBounceable + : true; + } catch { + return true; + } + } + + private static parseStateInit(stateInit?: string) { + if (!stateInit) { + return; + } + const { code, data } = loadStateInit(Cell.fromBase64(stateInit).asSlice()); + return { code, data }; + } + + static parseSignRawMessages(messages: SignRawMessage[]) { + return messages.map((message) => + internal({ + to: message.address, + value: BigInt(message.amount), + body: message.payload && Cell.fromBase64(message.payload), + bounce: this.getBounceFlagFromAddress(message.address), + init: TransactionService.parseStateInit(message.stateInit), + }), + ); + } + + static createTransfer(contract, transferParams: TransferParams) { + const transfer = contract.createTransfer({ + timeout: TransactionService.getTimeout(), + seqno: transferParams.seqno, + secretKey: transferParams.secretKey, + sendMode: + transferParams.sendMode ?? SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, + messages: transferParams.messages, + }); + + return TransactionService.externalMessage(contract, transferParams.seqno, transfer) + .toBoc() + .toString('base64'); + } +} diff --git a/packages/@core-js/src/utils/AmountFormatter/AmountFormatter.ts b/packages/@core-js/src/utils/AmountFormatter/AmountFormatter.ts index 4ac5732f1..a985e378f 100644 --- a/packages/@core-js/src/utils/AmountFormatter/AmountFormatter.ts +++ b/packages/@core-js/src/utils/AmountFormatter/AmountFormatter.ts @@ -38,9 +38,9 @@ export class AmountFormatter { }; static sign = { - minus: '−', + minus: '−', plus: '+', - } + }; constructor(options: AmountFormatterOptions) { if (options.getDefaultDecimals) { @@ -71,13 +71,17 @@ export class AmountFormatter { return bn.shiftedBy(decimals ?? 9).toString(10); } - public fromNano(amount: AmountNumber, decimals: number = 9) { + static fromNanoStatic(amount: AmountNumber, decimals: number = 9) { return new BigNumber(amount ?? 0) .shiftedBy(-decimals) .decimalPlaces(decimals, BigNumber.ROUND_DOWN) .toString(10); } + public fromNano(amount: AmountNumber, decimals: number = 9) { + return AmountFormatter.fromNanoStatic(amount, decimals); + } + private toBN(amount: AmountNumber = 0) { return BigNumber.isBigNumber(amount) ? amount : new BigNumber(amount); } diff --git a/packages/@core-js/src/utils/tonapiUtils.ts b/packages/@core-js/src/utils/tonapiUtils.ts new file mode 100644 index 000000000..5ab449f01 --- /dev/null +++ b/packages/@core-js/src/utils/tonapiUtils.ts @@ -0,0 +1,5 @@ +import { Account } from '../TonAPI'; + +export function isActiveAccount(status: Account['status']) { + return !['empty', 'uninit', 'nonexist'].includes(status); +} diff --git a/packages/mobile/Gemfile b/packages/mobile/Gemfile index 36647872e..5d0a3998e 100644 --- a/packages/mobile/Gemfile +++ b/packages/mobile/Gemfile @@ -2,4 +2,6 @@ source 'https://rubygems.org' # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version ruby ">= 2.6.10" gem 'cocoapods', '~> 1.13' -gem 'activesupport', '>= 6.1.7.3', '< 7.1.0' \ No newline at end of file +gem 'activesupport', '>= 6.1.7.3', '< 7.1.0' +gem "fastlane" +gem "fastlane-plugin-increment_version_code" diff --git a/packages/mobile/Gemfile.lock b/packages/mobile/Gemfile.lock index ec07103c8..1577ec178 100644 --- a/packages/mobile/Gemfile.lock +++ b/packages/mobile/Gemfile.lock @@ -13,7 +13,25 @@ GEM algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) + artifactory (3.0.15) atomos (0.1.3) + aws-eventstream (1.2.0) + aws-partitions (1.855.0) + aws-sdk-core (3.187.1) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.72.0) + aws-sdk-core (~> 3, >= 3.184.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.138.0) + aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.6) + aws-sigv4 (1.6.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) claide (1.1.0) cocoapods (1.13.0) addressable (~> 2.8) @@ -52,31 +70,191 @@ GEM nap (>= 0.8, < 2.0) netrc (~> 0.11) cocoapods-try (1.2.0) + colored (1.2) colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) concurrent-ruby (1.2.2) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20231109) + dotenv (2.8.1) + emoji_regex (3.2.3) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) + excon (0.104.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.2.7) + fastlane (2.217.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (~> 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-increment_version_code (0.4.3) ffi (1.16.1) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.53.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.2) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.29.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.45.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.29.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + domain_name (~> 0.5) httpclient (2.8.3) i18n (1.14.1) concurrent-ruby (~> 1.0) + jmespath (1.6.2) json (2.6.3) + jwt (2.7.1) + mini_magick (4.12.0) + mini_mime (1.1.5) minitest (5.16.3) molinillo (0.8.0) + multi_json (1.15.0) + multipart-post (2.3.0) nanaimo (0.3.0) nap (1.1.0) + naturally (2.2.1) netrc (0.11.0) + optparse (0.1.1) + os (1.1.4) + plist (3.7.0) public_suffix (4.0.7) - rexml (3.2.5) + rake (13.1.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.6) + rouge (2.0.7) ruby-macho (2.5.1) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + signet (0.18.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) typhoeus (1.4.0) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + uber (0.1.0) + unicode-display_width (2.5.0) + webrick (1.8.1) + word_wrap (1.0.0) xcodeproj (1.23.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) @@ -84,6 +262,10 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) PLATFORMS arm64-darwin-23 @@ -92,6 +274,8 @@ PLATFORMS DEPENDENCIES activesupport (>= 6.1.7.3, < 7.1.0) cocoapods (~> 1.13) + fastlane + fastlane-plugin-increment_version_code RUBY VERSION ruby 3.2.2p53 diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 335e28461..8bdce0af6 100644 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -91,11 +91,20 @@ android { applicationId "com.ton_keeper" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 392 - versionName "3.4.4" + versionCode 424 + versionName "3.5" missingDimensionStrategy 'react-native-camera', 'general' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget=11 + } + signingConfigs { release { if (project.hasProperty('TONKEEPER_UPLOAD_STORE_FILE')) { @@ -103,6 +112,11 @@ android { storePassword TONKEEPER_UPLOAD_STORE_PASSWORD keyAlias TONKEEPER_UPLOAD_KEY_ALIAS keyPassword TONKEEPER_UPLOAD_KEY_PASSWORD + } else { + storeFile file(project.property('android.injected.signing.store.file')) + storePassword project.property('android.injected.signing.store.password') + keyAlias project.property('android.injected.signing.key.alias') + keyPassword project.property('android.injected.signing.key.password') } } debug { diff --git a/packages/mobile/android/app/src/main/AndroidManifest.xml b/packages/mobile/android/app/src/main/AndroidManifest.xml index 6a16d6121..ff3b3375c 100644 --- a/packages/mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/mobile/android/app/src/main/AndroidManifest.xml @@ -26,6 +26,7 @@ android:allowBackup="false" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" + android:localeConfig="@xml/locales_config" android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" @@ -83,6 +84,7 @@ + @@ -100,6 +102,9 @@ android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false"/> + diff --git a/packages/mobile/android/app/src/main/res/xml/locales_config.xml b/packages/mobile/android/app/src/main/res/xml/locales_config.xml new file mode 100644 index 000000000..54caa1fa5 --- /dev/null +++ b/packages/mobile/android/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/mobile/android/fastlane/Appfile b/packages/mobile/android/fastlane/Appfile new file mode 100644 index 000000000..f1d2d100b --- /dev/null +++ b/packages/mobile/android/fastlane/Appfile @@ -0,0 +1,2 @@ +app_identifier('com.ton_keeper') + diff --git a/packages/mobile/android/fastlane/Fastfile b/packages/mobile/android/fastlane/Fastfile new file mode 100644 index 000000000..05be5e20a --- /dev/null +++ b/packages/mobile/android/fastlane/Fastfile @@ -0,0 +1,92 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + + +platform :android do + + desc "Fetches the latest version code from the Play Console and increments it by 1" + lane :fetch_and_increment_build_number do + app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) + + version_codes = google_play_track_version_codes( + package_name: app_identifier, + track: "internal", + json_key: ENV["ANDROID_JSON_KEY_FILE"] + ) + + updated_version_code = version_codes[0] + 1 + + increment_version_code( + gradle_file_path: "./app/build.gradle", + version_code: updated_version_code + ) + + sh("echo VERSION_CODE=#{updated_version_code} >> $GITHUB_ENV") + end + + desc "Build the android aab for release" + lane :build_release do |options| + gradle( + task: "bundle", + build_type: "Release", + properties: { + "android.injected.signing.store.file" => ENV["KEYSTORE_FILE"], + "android.injected.signing.store.password" => ENV["KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => ENV["KEY_ALIAS"], + "android.injected.signing.key.password" => ENV["KEY_PASSWORD"], + } + ) + end + + desc "Build the android apk" + lane :assemble_release do |options| + gradle( + task: "assemble", + build_type: "Release", + properties: { + "android.injected.signing.store.file" => ENV["KEYSTORE_FILE"], + "android.injected.signing.store.password" => ENV["KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => ENV["KEY_ALIAS"], + "android.injected.signing.key.password" => ENV["KEY_PASSWORD"], + } + ) + end + + desc "Upload to GooglePlay" + lane :upload_release do + aab_path = lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH] + app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) + + upload_to_play_store( + track: "internal", + json_key: ENV["ANDROID_JSON_KEY_FILE"], + aab: aab_path, + package_name: app_identifier, + ) + end + + desc "Build and upload to GooglePlay" + lane :beta do + fetch_and_increment_build_number + build_release + upload_release + end + + desc "Build APK" + lane :apk do + fetch_and_increment_build_number + assemble_release + end +end \ No newline at end of file diff --git a/packages/mobile/ios/Podfile b/packages/mobile/ios/Podfile index 5727ccf6f..9a35b48c3 100644 --- a/packages/mobile/ios/Podfile +++ b/packages/mobile/ios/Podfile @@ -62,7 +62,7 @@ target 'ton_keeper' do # Utilities pod 'R.swift', '~> 6.1.0' - pod 'TonMnemonicSwift', :git => 'git@github.com:tonkeeper/ton-mnemonic-swift.git', :branch => 'main' + pod 'TonMnemonicSwift', :git => 'https://github.com/tonkeeper/ton-mnemonic-swift.git', :branch => 'main' permissions_path = '../node_modules/react-native-permissions/ios' pod 'Permission-Camera', :path => "#{permissions_path}/Camera" diff --git a/packages/mobile/ios/fastlane/Appfile b/packages/mobile/ios/fastlane/Appfile new file mode 100644 index 000000000..d0ed3cc95 --- /dev/null +++ b/packages/mobile/ios/fastlane/Appfile @@ -0,0 +1,3 @@ +app_identifier(ENV["DEVELOPER_APP_IDENTIFIER"]) +team_id(ENV["DEVELOPER_TEAM_ID"]) + diff --git a/packages/mobile/ios/fastlane/Fastfile b/packages/mobile/ios/fastlane/Fastfile new file mode 100644 index 000000000..63345d50b --- /dev/null +++ b/packages/mobile/ios/fastlane/Fastfile @@ -0,0 +1,157 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +X_CODE = ENV["X_CODE"] +if X_CODE != '' + xcode_select("/Applications/Xcode_#{X_CODE}.app") +end + +platform :ios do + desc "Load ASC API Key information to use in subsequent lanes" + lane :load_asc_api_key do + app_store_connect_api_key( + key_id: ENV["ASC_KEY_ID"], + issuer_id: ENV["ASC_ISSUER_ID"], + key_content: ENV["ASC_KEY"], + duration: 500, # maximum 1200 + is_key_content_base64: false, + in_house: false # detecting this via ASC private key not currently supported + ) + end + + desc "Installs signing certificate in the keychain and downloads provisioning profiles from App Store Connect" + lane :prepare_signing do |options| + api_key = lane_context[SharedValues::APP_STORE_CONNECT_API_KEY] + app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) + + keychain_name = "login.keychain" + keychain_password = ENV["SIGNING_KEY_PASSWORD"] + + delete_keychain( + name: keychain_name + ) if File.exist? File.expand_path("~/Library/Keychains/#{keychain_name}-db") + + create_keychain( + name: keychain_name, + password: keychain_password, + default_keychain: true, + unlock: true, + timeout: 3600 + ) + + import_certificate( + certificate_path: ENV["SIGNING_KEY_FILE_PATH"], + certificate_password: ENV["SIGNING_KEY_PASSWORD"], + keychain_name: keychain_name, + keychain_password: keychain_password + ) + + # fetches and installs provisioning profiles from ASC + sigh( + adhoc: options[:adhoc], + api_key: api_key, + app_identifier: app_identifier, + readonly: true + ) + end + + desc "Bump build number based on most recent TestFlight build number" + lane :fetch_and_increment_build_number do + #fetch read your app identifier defined in your Appfile + app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) + api_key = lane_context[SharedValues::APP_STORE_CONNECT_API_KEY] + + current_version = get_version_number( + xcodeproj: "ton_keeper.xcodeproj" + ) + latest_build_number = latest_testflight_build_number( + api_key: api_key, + version: current_version, + app_identifier: app_identifier + ) + + updated_version_code = latest_build_number + 1 + + increment_build_number( + build_number: updated_version_code, + ) + + sh("echo VERSION_CODE=#{updated_version_code} >> $GITHUB_ENV") + end + + desc "Build the iOS app for release" + lane :build_release do |options| + app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) + team_id = CredentialsManager::AppfileConfig.try_fetch_value(:team_id) + + output_name = "ton_keeper" # specify the name of the .ipa file to generate + export_method = "app-store" # specify the export method + appstore_provision_file = "match AppStore " + app_identifier + + # turn off automatic signing during build so correct code signing identity is guaranteed to be used + update_code_signing_settings( + path: "ton_keeper.xcodeproj", + use_automatic_signing: false, + team_id: team_id, + targets: ["ton_keeper"], + code_sign_identity: "iPhone Distribution", # replace with name of code signing identity if different + bundle_identifier: app_identifier, + build_configurations: ["Release"] # only toggle code signing settings for Release configurations + ) + + update_project_provisioning( + xcodeproj: "ton_keeper.xcodeproj", + profile: "AppStore_" + app_identifier + ".mobileprovision", + build_configuration: "Release", + code_signing_identity: "iPhone Distribution" + ) + + # build the app + gym( + scheme: "ton_keeper", # replace with name of your project’s scheme + output_name: output_name, + configuration: "Release", + export_method: export_method, + export_options: { + provisioningProfiles: { + app_identifier => appstore_provision_file + } + } + ) + end + + desc "Upload to TestFlight / ASC" + lane :upload_release do + api_key = lane_context[SharedValues::APP_STORE_CONNECT_API_KEY] + + deliver( + api_key: api_key, + skip_screenshots: true, + skip_metadata: true, + skip_app_version_update: true, + force: true, # skips verification of HTML preview file (since this will be run from a CI machine) + run_precheck_before_submit: false # not supported through ASC API yet + ) + end + + desc "Build and upload to TestFlight" + lane :beta do + load_asc_api_key + prepare_signing + fetch_and_increment_build_number + build_release + upload_release + end +end \ No newline at end of file diff --git a/packages/mobile/ios/fastlane/README.md b/packages/mobile/ios/fastlane/README.md new file mode 100644 index 000000000..938c27fb0 --- /dev/null +++ b/packages/mobile/ios/fastlane/README.md @@ -0,0 +1,72 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios load_asc_api_key + +```sh +[bundle exec] fastlane ios load_asc_api_key +``` + +Load ASC API Key information to use in subsequent lanes + +### ios prepare_signing + +```sh +[bundle exec] fastlane ios prepare_signing +``` + +Installs signing certificate in the keychain and downloads provisioning profiles from App Store Connect + +### ios fetch_and_increment_build_number + +```sh +[bundle exec] fastlane ios fetch_and_increment_build_number +``` + +Bump build number based on most recent TestFlight build number + +### ios build_release + +```sh +[bundle exec] fastlane ios build_release +``` + +Build the iOS app for release + +### ios upload_release + +```sh +[bundle exec] fastlane ios upload_release +``` + +Upload to TestFlight / ASC + +### ios beta + +```sh +[bundle exec] fastlane ios beta +``` + +Build and upload to TestFlight + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj index 9db8601b1..2ee59a456 100644 --- a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj @@ -1284,7 +1284,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = ton_keeper/ton_keeper.entitlements; - CURRENT_PROJECT_VERSION = 393; + CURRENT_PROJECT_VERSION = 423; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = CT523DK2KC; ENABLE_BITCODE = NO; @@ -1294,7 +1294,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.4.4; + MARKETING_VERSION = 3.5; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1319,7 +1319,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = ton_keeper/ton_keeper.entitlements; - CURRENT_PROJECT_VERSION = 393; + CURRENT_PROJECT_VERSION = 423; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = CT523DK2KC; INFOPLIST_FILE = ton_keeper/SupportingFiles/Info.plist; @@ -1328,7 +1328,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.4.4; + MARKETING_VERSION = 3.5; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1409,7 +1409,11 @@ ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_CPLUSPLUSFLAGS = "$(inherited)"; - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; }; @@ -1474,7 +1478,11 @@ MTL_ENABLE_DEBUG_INFO = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_CPLUSPLUSFLAGS = "$(inherited)"; - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/packages/mobile/ios/ton_keeper/ton_keeper.entitlements b/packages/mobile/ios/ton_keeper/ton_keeper.entitlements index 5a73d4f98..2c581255e 100644 --- a/packages/mobile/ios/ton_keeper/ton_keeper.entitlements +++ b/packages/mobile/ios/ton_keeper/ton_keeper.entitlements @@ -8,6 +8,7 @@ applinks:app.tonkeeper.org applinks:app.tonkeeper.com + applinks:ton.app diff --git a/packages/mobile/package.json b/packages/mobile/package.json index b651d6b80..e33a9df6a 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -12,7 +12,7 @@ "clear-cache": "rm -rf tmp/haste-map-react-native-packager && rm -rf node_modules && yarn && yarn start --reset-cache", "build:android": "cd android && ./gradlew assembleRelease && ./gradlew bundleRelease && cd .. && node scripts/prepare_builds.js", "build:android:apk": "cd android && ./gradlew assembleRelease && cd ..", - "build:android:google-play": "git apply ../../patches/google-play-release.patch && yarn build:android:apk && git apply -R ../../patches/google-play-release.patch", + "build:android:google-play": "git apply ../../patches/google-play-release.patch && yarn build:android && git apply -R ../../patches/google-play-release.patch", "build:android:apk-site": "ENVFILE=.env.site yarn build:android:apk", "clean:android": "cd android && ./gradlew clean && cd ..", "open_build_folder": "open ./android/app/build/outputs/bundle", @@ -31,7 +31,7 @@ "@craftzdog/react-native-buffer": "^6.0.5", "@expo/react-native-action-sheet": "^4.0.1", "@gorhom/bottom-sheet": "^4.4.7", - "@rainbow-me/animated-charts": "git+ssh://git@github.com:tonkeeper/react-native-animated-charts#65f723604f3abc8a05ecfa2918fe9b0b42fd8363", + "@rainbow-me/animated-charts": "https://github.com/tonkeeper/react-native-animated-charts#65f723604f3abc8a05ecfa2918fe9b0b42fd8363", "@react-native-async-storage/async-storage": "^1.15.5", "@react-native-community/clipboard": "^1.5.1", "@react-native-community/netinfo": "^9.3.2", @@ -41,6 +41,8 @@ "@react-native-firebase/messaging": "^18.5.0", "@reduxjs/toolkit": "^1.6.1", "@shopify/flash-list": "^1.5.0", + "@ton/core": "^0.53.0", + "@ton/ton": "^13.9.0", "@tonapps/tonlogin-client": "0.2.5", "@tonconnect/protocol": "^2.2.5", "@tonkeeper/core": "0.1.0", @@ -90,7 +92,7 @@ "react-native-console-time-polyfill": "^1.2.3", "react-native-crypto": "^2.2.0", "react-native-device-info": "^8.3.1", - "react-native-encrypted-storage": "git+ssh://git@github.com:tonkeeper/react-native-encrypted-storage#6d2dd34fed3438364125175a32c6f4f3d018078e", + "react-native-encrypted-storage": "https://github.com/tonkeeper/react-native-encrypted-storage#6d2dd34fed3438364125175a32c6f4f3d018078e", "react-native-exception-handler": "^2.10.10", "react-native-fast-image": "^8.5.11", "react-native-fs": "^2.20.0", @@ -103,7 +105,7 @@ "react-native-linear-gradient": "^2.6.2", "react-native-localize": "^2.2.4", "react-native-minimizer": "1.3.0", - "react-native-pager-view": "git+ssh://git@github.com:bogoslavskiy/react-native-pager-view#78c0bb573fce185f6f51bae6c1c566a1ec6294eb", + "react-native-pager-view": "https://github.com/bogoslavskiy/react-native-pager-view#78c0bb573fce185f6f51bae6c1c566a1ec6294eb", "react-native-permissions": "3.6.1", "react-native-qrcode-scanner": "^1.5.5", "react-native-quick-base64": "^2.0.7", @@ -130,8 +132,6 @@ "stream-browserify": "^3.0.0", "styled-components": "^5.3.0", "text-encoding-polyfill": "^0.6.7", - "ton": "^13.5.0", - "ton-core": "^0.50.0", "tonapi-sdk-js": "^0.24.0", "tonweb": "^0.0.58", "tweetnacl": "^1.0.3", @@ -183,6 +183,7 @@ "react-native-minimizer", "react-native-scrypt", "react-native-camera", + "react-native-pager-view", "tonweb", "expo-modules-core", "@unimodules/core", diff --git a/packages/mobile/src/blockchain/contractService.ts b/packages/mobile/src/blockchain/contractService.ts deleted file mode 100644 index 2cdfbd88c..000000000 --- a/packages/mobile/src/blockchain/contractService.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { WalletContractV3R1, WalletContractV3R2, WalletContractV4 } from 'ton'; -import { Address, beginCell, Cell, comment, external, storeMessage } from 'ton-core'; -import { WalletVersion } from './types'; -import { Vault } from './vault'; - -const workchain = 0; - -export const walletContract = (publicKey: Buffer, version: WalletVersion) => { - switch (version) { - case WalletVersion.v3R1: - return WalletContractV3R1.create({ workchain, publicKey }); - case WalletVersion.v3R2: - return WalletContractV3R2.create({ workchain, publicKey }); - case WalletVersion.v4R1: - throw new Error('Unsupported wallet contract version - v4R1'); - case WalletVersion.v4R2: - return WalletContractV4.create({ workchain, publicKey }); - } -}; - -export const contractVersionsMap = { - v4R2: WalletVersion.v4R2, - v4R1: WalletVersion.v4R1, - v3R2: WalletVersion.v3R2, - v3R1: WalletVersion.v3R1, -}; - -export const getTonCoreWalletContract = (vault: Vault, version = 'v4R2') => { - return walletContract(Buffer.from(vault.tonPublicKey), contractVersionsMap[version]); -}; - -export const externalMessage = ( - contract: WalletContractV3R1 | WalletContractV3R2 | WalletContractV4, - seqno: number, - body: Cell, -) => { - return beginCell() - .storeWritable( - storeMessage( - external({ - to: contract.address, - init: seqno === 0 ? contract.init : undefined, - body: body, - }), - ), - ) - .endCell(); -}; - -export const jettonTransferBody = (params: { - queryId?: number; - jettonAmount: bigint; - toAddress: Address; - responseAddress: Address; - forwardAmount: bigint; - forwardPayload: Cell | string; -}) => { - let forwardPayload = - typeof params.forwardPayload === 'string' && params.forwardPayload.length > 0 - ? comment(params.forwardPayload) - : null; - - if (params.forwardPayload instanceof Cell) { - forwardPayload = params.forwardPayload; - } - - return beginCell() - .storeUint(0xf8a7ea5, 32) // request_transfer op - .storeUint(params.queryId || 0, 64) - .storeCoins(params.jettonAmount) - .storeAddress(params.toAddress) - .storeAddress(params.responseAddress) - .storeBit(false) // null custom_payload - .storeCoins(params.forwardAmount) - .storeBit(forwardPayload != null) // forward_payload in this slice - false, separate cell - true - .storeMaybeRef(forwardPayload) - .endCell(); -}; diff --git a/packages/mobile/src/blockchain/wallet.ts b/packages/mobile/src/blockchain/wallet.ts index e3d747430..51d218a15 100644 --- a/packages/mobile/src/blockchain/wallet.ts +++ b/packages/mobile/src/blockchain/wallet.ts @@ -5,7 +5,13 @@ import { getUnixTime } from 'date-fns'; import { store } from '$store'; import { getServerConfig } from '$shared/constants'; import { UnlockedVault, Vault } from './vault'; -import { Address as AddressFormatter, AmountFormatter } from '@tonkeeper/core'; +import { + Address as AddressFormatter, + ContractService, + contractVersionsMap, + TransactionService, + isActiveAccount, +} from '@tonkeeper/core'; import { debugLog } from '$utils/debugLog'; import { getChainName, getWalletName } from '$shared/dynamicConfig'; import { t } from '@tonkeeper/shared/i18n'; @@ -21,17 +27,25 @@ import { Account, } from '@tonkeeper/core/src/legacy'; import { SendApi, Configuration as V1Configuration } from 'tonapi-sdk-js'; -import { - externalMessage, - getTonCoreWalletContract, - jettonTransferBody, -} from './contractService'; -import { Address, Cell, SendMode, internal, toNano } from 'ton-core'; + +import { tk } from '@tonkeeper/shared/tonkeeper'; +import { Address, Cell, internal, toNano } from '@ton/core'; const TonWeb = require('tonweb'); export const jettonTransferAmount = toNano('0.64'); -const jettonTransferForwardAmount = BigInt('1'); + +interface TonTransferParams { + seqno: number; + recipient: Account; + amount: string; + payload?: Cell | string; + sendMode?: number; + vault: Vault; + walletVersion?: string | null; + secretKey?: Buffer; + bounce: boolean; +} export class Wallet { readonly name: string; @@ -346,34 +360,36 @@ export class TonWallet { vault: Vault, secretKey: Buffer = Buffer.alloc(64), ) { - const contract = getTonCoreWalletContract(vault, vault.getVersion()); + const version = vault.getVersion(); + const lockupConfig = vault.getLockupConfig(); + const contract = ContractService.getWalletContract( + contractVersionsMap[version ?? 'v4R2'], + Buffer.from(vault.tonPublicKey), + { + allowedDestinations: lockupConfig?.allowed_destinations, + }, + ); const jettonAmount = BigInt(amountNano); - const body = jettonTransferBody({ - queryId: Date.now(), - jettonAmount, - toAddress: Address.parseRaw(recipient.address), - responseAddress: Address.parseRaw(sender.address), - forwardAmount: jettonTransferForwardAmount, - forwardPayload: payload, - }); - - const transfer = contract.createTransfer({ + return TransactionService.createTransfer(contract, { seqno, secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, messages: [ internal({ to: Address.parse(jettonWalletAddress), bounce: true, value: jettonTransferAmount, - body: body, + body: ContractService.createJettonTransferBody({ + queryId: Date.now(), + jettonAmount, + receiverAddress: recipient.address, + excessesAddress: tk.wallet.address.ton.raw, + forwardBody: payload, + }), }), ], }); - - return externalMessage(contract, seqno, transfer).toBoc().toString('base64'); } async estimateJettonFee( @@ -491,52 +507,40 @@ export class TonWallet { }; } - async createTonTransfer( - seqno: number, - recipient: Account, - amount: string, - payload: Cell | string = '', + async createTonTransfer({ + seqno, + recipient, + amount, + payload = '', sendMode = 3, - vault: Vault, - walletVersion: string | null = null, - secretKey: Buffer = Buffer.alloc(64), - ) { + vault, + bounce, + walletVersion = null, + secretKey = Buffer.alloc(64), + }: TonTransferParams) { const version = vault.getVersion(); - const isLockup = version && version.substr(0, 6) === 'lockup'; - - if (isLockup) { - const wallet = vault.tonWallet; - - const tx = wallet.methods.transfer({ - secretKey, - toAddress: new TonWeb.utils.Address(recipient.address), - amount: AmountFormatter.toNano(amount), - seqno: seqno, - payload, - sendMode, - }); - - const query = await tx.getQuery(); - return TonWeb.utils.bytesToBase64(await query.toBoc(false)); - } else { - const contract = getTonCoreWalletContract(vault, walletVersion ?? version); - - const transfer = contract.createTransfer({ - seqno, - secretKey, - sendMode, - messages: [ - internal({ - to: Address.parseRaw(recipient.address), - bounce: recipient.status === 'active', - value: amount, - body: payload !== '' ? payload : undefined, - }), - ], - }); - - return externalMessage(contract, seqno, transfer).toBoc().toString('base64'); - } + const lockupConfig = vault.getLockupConfig(); + const contract = ContractService.getWalletContract( + contractVersionsMap[walletVersion ?? version ?? 'v4R2'], + Buffer.from(vault.tonPublicKey), + { + lockupPubKey: lockupConfig?.config_pubkey, + allowedDestinations: lockupConfig?.allowed_destinations, + }, + ); + return TransactionService.createTransfer(contract, { + seqno, + secretKey, + sendMode, + messages: [ + internal({ + to: recipient.address, + bounce, + value: amount, + body: payload !== '' ? payload : undefined, + }), + ], + }); } async estimateFee( @@ -559,15 +563,18 @@ export class TonWallet { throw new Error(t('send_get_wallet_info_error')); } - const boc = await this.createTonTransfer( + const boc = await this.createTonTransfer({ seqno, - recipientInfo, + recipient: recipientInfo, amount, payload, sendMode, vault, walletVersion, - ); + bounce: isActiveAccount(recipientInfo.status) + ? AddressFormatter.isBounceable(address) + : false, + }); let feeNano = await this.calcFee(boc); @@ -609,16 +616,20 @@ export class TonWallet { const amountNano = Ton.toNano(amount); - const boc = await this.createTonTransfer( + const boc = await this.createTonTransfer({ seqno, - recipientInfo, + recipient: recipientInfo, amount, payload, sendMode, - unlockedVault, + vault: unlockedVault, walletVersion, - Buffer.from(secretKey), - ); + secretKey: Buffer.from(secretKey), + // We should keep bounce flag from user input. We should check contract status till Jan 1, 2024 according to internal Address reform roadmap + bounce: isActiveAccount(recipientInfo.status) + ? AddressFormatter.isBounceable(address) + : false, + }); let feeNano: BigNumber; try { @@ -666,32 +677,40 @@ export class TonWallet { } async getLockupBalances(info: Account) { - if (['empty', 'uninit', 'nonexist'].includes(info?.status ?? '')) { - try { + try { + if (['empty', 'uninit', 'nonexist'].includes(info?.status ?? '')) { const balance = ( await this.blockchainApi.getRawAccount({ accountId: info.address }) ).balance; return [Ton.fromNano(balance), 0, 0]; - } catch (e) { - return [Ton.fromNano('0'), 0, 0]; } - } - const balances = await this.vault.tonWallet.getBalances(); - const result = balances.map((item: number) => Ton.fromNano(item.toString())); - result[0] = new BigNumber(result[0]).minus(result[1]).minus(result[2]).toString(); + const balances = await this.vault.tonWallet.getBalances(); + const result = balances.map((item: number) => Ton.fromNano(item.toString())); + result[0] = new BigNumber(result[0]).minus(result[1]).minus(result[2]).toString(); - return result; + return result; + } catch (e) { + if (e?.response?.status === 404) { + return [Ton.fromNano('0'), 0, 0]; + } + + throw e; + } } async getBalance(): Promise { - const account = await this.vault.getTonAddress(this.isTestnet); try { + const account = await this.vault.getTonAddress(this.isTestnet); const balance = (await this.blockchainApi.getRawAccount({ accountId: account })) .balance; return Ton.fromNano(balance); } catch (e) { - return Ton.fromNano('0'); + if (e?.response?.status === 404) { + return Ton.fromNano(0); + } + + throw e; } } } diff --git a/packages/mobile/src/core/ChooseCountry/ChooseCountry.tsx b/packages/mobile/src/core/ChooseCountry/ChooseCountry.tsx index fb8cc8444..41fa7666e 100644 --- a/packages/mobile/src/core/ChooseCountry/ChooseCountry.tsx +++ b/packages/mobile/src/core/ChooseCountry/ChooseCountry.tsx @@ -176,6 +176,7 @@ export const ChooseCountry: React.FC = () => { /> )} data={filteredListBySearch} + keyboardShouldPersistTaps="handled" /> ); diff --git a/packages/mobile/src/core/ChooseCountry/components/SearchNavBar.tsx b/packages/mobile/src/core/ChooseCountry/components/SearchNavBar.tsx index b4a83ac9e..5347298d4 100644 --- a/packages/mobile/src/core/ChooseCountry/components/SearchNavBar.tsx +++ b/packages/mobile/src/core/ChooseCountry/components/SearchNavBar.tsx @@ -10,7 +10,7 @@ import { } from '@tonkeeper/uikit'; import Animated, { SharedValue, useAnimatedStyle } from 'react-native-reanimated'; import { useTheme } from '$hooks/useTheme'; -import { LayoutAnimation } from 'react-native'; +import { LayoutAnimation, StyleSheet } from 'react-native'; import { NavBar } from '$uikit'; export interface SearchNavBarProps { @@ -101,7 +101,7 @@ const styles = Steezy.create({ }, borderContainer: { zIndex: 2, - borderBottomWidth: 0.5, + borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: 'transparent', }, cancelContainer: { diff --git a/packages/mobile/src/core/DAppBrowser/components/BrowserNavBar/BrowserNavBar.tsx b/packages/mobile/src/core/DAppBrowser/components/BrowserNavBar/BrowserNavBar.tsx index 83f1444aa..c62cb9cf1 100644 --- a/packages/mobile/src/core/DAppBrowser/components/BrowserNavBar/BrowserNavBar.tsx +++ b/packages/mobile/src/core/DAppBrowser/components/BrowserNavBar/BrowserNavBar.tsx @@ -63,9 +63,11 @@ const BrowserNavBarComponent: FC = (props) => { const domain = getDomainFromURL(url); - const shortAddress = Address.parse(walletAddress, { - bounceable: !getFlag('address_style_nobounce'), - }).toShort(); + const shortAddress = + walletAddress && + Address.parse(walletAddress, { + bounceable: !getFlag('address_style_nobounce'), + }).toShort(); const popupItems = useMemo(() => { const items: PopupAction[] = [ diff --git a/packages/mobile/src/core/DAppsExplore/DAppsExplore.tsx b/packages/mobile/src/core/DAppsExplore/DAppsExplore.tsx index 766ebfbd1..4afe8de01 100644 --- a/packages/mobile/src/core/DAppsExplore/DAppsExplore.tsx +++ b/packages/mobile/src/core/DAppsExplore/DAppsExplore.tsx @@ -35,6 +35,7 @@ import { Text as RNText } from 'react-native'; import { ScrollPositionContext } from '$uikit'; import { useFocusEffect, useTabPress } from '@tonkeeper/router'; import { useSelectedCountry } from '$store/zustand/methodsToBuy/useSelectedCountry'; +import { CountryButton } from '@tonkeeper/shared/components'; export type DAppsExploreProps = NativeStackScreenProps< BrowserStackParamList, @@ -159,14 +160,12 @@ const DAppsExploreComponent: FC = (props) => { + + + } > ({ container: { flex: 1, }, - regionButton: { - marginRight: 16, + countryButtonContainer: { + flex: 1, + justifyContent: 'center', + minWidth: 64, + alignItems: 'flex-end', + paddingRight: 16, }, segmentedControl: { backgroundColor: 'transparent', diff --git a/packages/mobile/src/core/Exchange/Exchange.style.ts b/packages/mobile/src/core/Exchange/Exchange.style.ts index 84d4ceec2..93b2399e6 100644 --- a/packages/mobile/src/core/Exchange/Exchange.style.ts +++ b/packages/mobile/src/core/Exchange/Exchange.style.ts @@ -33,3 +33,7 @@ export const OtherWaysButtonLabel = styled(Text).attrs(() => ({ variant: 'body2', color: 'foregroundSecondary', }))``; + +export const TitleContainer = styled.View` + padding: ${ns(14)}px ${ns(16)}px; +`; diff --git a/packages/mobile/src/core/Exchange/ExchangeItem/ExchangeItem.style.ts b/packages/mobile/src/core/Exchange/ExchangeItem/ExchangeItem.style.ts index 13d9e4317..6bf801cb1 100644 --- a/packages/mobile/src/core/Exchange/ExchangeItem/ExchangeItem.style.ts +++ b/packages/mobile/src/core/Exchange/ExchangeItem/ExchangeItem.style.ts @@ -83,3 +83,35 @@ export const Badge = styled.View` right: ${ns(3)}px; z-index: 3; `; + +export const AssetsContainer = styled.View` + flex-direction: row; + margin-top: ${ns(4)}px; + margin-left: -${ns(2)}px; +`; + +export const Asset = styled.View` + width: ${ns(28)}px; + height: ${ns(28)}px; + border-radius: ${ns(28) / 2}px; + border-width: ${ns(2)}px; + border-color: ${({ theme }) => theme.colors.backgroundSecondary}; + background: ${({ theme }) => theme.colors.backgroundTertiary}; + margin-right: -${ns(8)}px; + overflow: hidden; +`; + +export const AssetImage = styled.Image` + width: ${ns(24)}px; + height: ${ns(24)}px; +`; + +export const AssetsCount = styled.View` + height: ${ns(24)}px; + border-radius: ${ns(24) / 2}px; + padding: 0 ${ns(8)}px; + justify-content: center; + background: ${({ theme }) => theme.colors.backgroundTertiary}; + margin-left: ${ns(14)}px; + margin-top: ${ns(2)}px; +`; diff --git a/packages/mobile/src/core/Exchange/ExchangeItem/ExchangeItem.tsx b/packages/mobile/src/core/Exchange/ExchangeItem/ExchangeItem.tsx index aed3b5de8..6b9c1cad3 100644 --- a/packages/mobile/src/core/Exchange/ExchangeItem/ExchangeItem.tsx +++ b/packages/mobile/src/core/Exchange/ExchangeItem/ExchangeItem.tsx @@ -8,6 +8,7 @@ import { Icon, Text } from '$uikit'; import { Linking } from 'react-native'; import { t } from '@tonkeeper/shared/i18n'; import { openExchangeMethodModal } from '$core/ModalContainer/ExchangeMethod/ExchangeMethod'; +import { getCryptoAssetIconSource } from '@tonkeeper/uikit/assets/cryptoAssets'; export const ExchangeItem: FC = ({ methodId, @@ -20,7 +21,9 @@ export const ExchangeItem: FC = ({ const isBot = methodId.endsWith('_bot'); const handlePress = useCallback(() => { - if (!method) return null; + if (!method) { + return null; + } if (isBot) { openExchangeMethodModal(methodId, () => { Linking.openURL(method.action_button.url); @@ -31,7 +34,9 @@ export const ExchangeItem: FC = ({ }, [isBot, method, methodId]); function renderBadge() { - if (!method) return null; + if (!method) { + return null; + } if (method.badge) { let backgroundColor = theme.colors.accentPrimary; if (method.badgeStyle === 'red') { @@ -67,18 +72,35 @@ export const ExchangeItem: FC = ({ {method.title} {isBot ? {t('exchange_telegram_bot')} : null} - - {method.subtitle} - + {method.assets ? ( + + {method.assets.slice(0, 3).map((asset, index) => ( + + + + ))} + {method.assets.length > 3 ? ( + + + + {method.assets.length} + + + ) : null} + + ) : ( + + {method.subtitle} + + )} - + diff --git a/packages/mobile/src/core/ModalContainer/ExchangeMethod/ExchangeMethod.tsx b/packages/mobile/src/core/ModalContainer/ExchangeMethod/ExchangeMethod.tsx index 4ece9c92d..2f90d3397 100644 --- a/packages/mobile/src/core/ModalContainer/ExchangeMethod/ExchangeMethod.tsx +++ b/packages/mobile/src/core/ModalContainer/ExchangeMethod/ExchangeMethod.tsx @@ -25,13 +25,13 @@ export const ExchangeMethod: FC = ({ methodId, onContinue } const nav = useNavigation(); const handleContinue = useCallback(() => { - // nav.goBack(); + nav.goBack(); setTimeout(() => { if (!wallet) { return openRequireWalletModal(); } else { - trackEvent(`exchange_open`, { internal_id: methodId }); + trackEvent('exchange_open', { internal_id: methodId }); if (onContinue) { onContinue(); @@ -46,7 +46,7 @@ export const ExchangeMethod: FC = ({ methodId, onContinue } if (isDontShow) { ExchangeDB.dontShowDetails(methodId); } - }, [isDontShow, wallet, methodId, onContinue]); + }, [nav, isDontShow, wallet, methodId, onContinue]); const handleLinkPress = useCallback( (url) => () => { @@ -55,6 +55,10 @@ export const ExchangeMethod: FC = ({ methodId, onContinue } [], ); + if (method === null) { + return null; + } + return ( @@ -75,15 +79,17 @@ export const ExchangeMethod: FC = ({ methodId, onContinue } {t('exchange_method_open_warning')} - - {method.info_buttons.map((item) => ( - - - {item.title} - - - ))} - + {method.info_buttons && method.info_buttons.length > 0 ? ( + + {method.info_buttons.map((item) => ( + + + {item.title} + + + ))} + + ) : null} diff --git a/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx b/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx index 091296b1e..3ab65d2d4 100644 --- a/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx +++ b/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx @@ -32,10 +32,19 @@ export interface InsufficientFundsParams { currency?: string; stakingFee?: string; fee?: string; + isStakingDeposit?: boolean; } export const InsufficientFundsModal = memo((props) => { - const { totalAmount, balance, currency = 'TON', decimals = 9, stakingFee, fee } = props; + const { + totalAmount, + balance, + currency = 'TON', + decimals = 9, + stakingFee, + fee, + isStakingDeposit, + } = props; const nav = useNavigation(); const formattedAmount = useMemo( () => formatter.format(fromNano(totalAmount, decimals), { decimals }), @@ -58,6 +67,48 @@ export const InsufficientFundsModal = memo((props) => { openExploreTab('defi'); }, [nav]); + const content = useMemo(() => { + if (isStakingDeposit) { + return ( + + {t('txActions.signRaw.insufficientFunds.stakingDeposit', { + amount: formattedAmount, + currency, + })} + {t('txActions.signRaw.insufficientFunds.yourBalance', { + balance: formattedBalance, + currency, + })} + + ); + } + + if (stakingFee && fee) { + return ( + + {t('txActions.signRaw.insufficientFunds.stakingFee', { + count: Number(stakingFee), + fee, + })} + + ); + } + + return ( + + {t('txActions.signRaw.insufficientFunds.toBePaid', { + amount: formattedAmount, + currency, + })} + {currency === 'TON' && t('txActions.signRaw.insufficientFunds.withFees')} + {t('txActions.signRaw.insufficientFunds.yourBalance', { + balance: formattedBalance, + currency, + })} + + ); + }, [currency, fee, formattedAmount, formattedBalance, isStakingDeposit, stakingFee]); + return ( @@ -67,26 +118,7 @@ export const InsufficientFundsModal = memo((props) => { {t('txActions.signRaw.insufficientFunds.title')} - {stakingFee && fee ? ( - - {t('txActions.signRaw.insufficientFunds.stakingFee', { - count: Number(stakingFee), - fee, - })} - - ) : ( - - {t('txActions.signRaw.insufficientFunds.toBePaid', { - amount: formattedAmount, - currency, - })} - {currency === 'TON' && t('txActions.signRaw.insufficientFunds.withFees')} - {t('txActions.signRaw.insufficientFunds.yourBalance', { - balance: formattedBalance, - currency, - })} - - )} + {content} diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/GetGemsSaleContract.ts b/packages/mobile/src/core/ModalContainer/NFTOperations/GetGemsSaleContract.ts deleted file mode 100644 index cdf1ebf92..000000000 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/GetGemsSaleContract.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Address } from "tonweb/dist/types/utils/address"; -import TonWeb, { ContractOptions } from "tonweb" -import BN from "bn.js"; - -const { Cell } = TonWeb.boc; -const { Contract } = TonWeb; - -// Fix tonweb typo -type WriteAddressMethod = (address?: Address | null) => void; -export interface GetGemsSaleContractOpetions extends ContractOptions { - marketplaceFeeAddress: Address; - marketplaceAddress: Address; - royaltyAddress: Address; - nftItemAddress: Address; - marketplaceFee: BN; - royaltyAmount: BN; - fullPrice: BN; - createdAt: number; -} - -export class GetGemsSaleContract extends Contract { - constructor(provider, options: any) { - options.wc = 0; - - const NftFixPriceSaleV2CodeBoc = 'te6cckECDAEAAikAART/APSkE/S88sgLAQIBIAMCAATyMAIBSAUEAFGgOFnaiaGmAaY/9IH0gfSB9AGoYaH0gfQB9IH0AGEEIIySsKAVgAKrAQICzQgGAfdmCEDuaygBSYKBSML7y4cIk0PpA+gD6QPoAMFOSoSGhUIehFqBSkHCAEMjLBVADzxYB+gLLaslx+wAlwgAl10nCArCOF1BFcIAQyMsFUAPPFgH6AstqyXH7ABAjkjQ04lpwgBDIywVQA88WAfoCy2rJcfsAcCCCEF/MPRSBwCCIYAYyMsFKs8WIfoCy2rLHxPLPyPPFlADzxbKACH6AsoAyYMG+wBxVVAGyMsAFcsfUAPPFgHPFgHPFgH6AszJ7VQC99AOhpgYC42EkvgnB9IBh2omhpgGmP/SB9IH0gfQBqGBNgAPloyhFrpOEBWccgGRwcKaDjgskvhHAoomOC+XD6AmmPwQgCicbIiV15cPrpn5j9IBggKwNkZYAK5Y+oAeeLAOeLAOeLAP0BZmT2qnAbE+OAcYED6Y/pn5gQwLCQFKwAGSXwvgIcACnzEQSRA4R2AQJRAkECPwBeA6wAPjAl8JhA/y8AoAyoIQO5rKABi+8uHJU0bHBVFSxwUVsfLhynAgghBfzD0UIYAQyMsFKM8WIfoCy2rLHxnLPyfPFifPFhjKACf6AhfKAMmAQPsAcQZQREUVBsjLABXLH1ADzxYBzxYBzxYB+gLMye1UABY3EDhHZRRDMHDwBTThaBI=' - const NftFixPriceSaleV2CodeCell = Cell.oneFromBoc(TonWeb.utils.base64ToBytes(NftFixPriceSaleV2CodeBoc)); - - options.code = NftFixPriceSaleV2CodeCell; - - super(provider, options); - } - - protected createDataCell() { - let feesCell = new Cell(); - - feesCell.bits.writeAddress(this.options.marketplaceFeeAddress); - feesCell.bits.writeCoins(this.options.marketplaceFee); - feesCell.bits.writeAddress(this.options.royaltyAddress); - feesCell.bits.writeCoins(this.options.royaltyAmount); - - let dataCell = new Cell(); - - dataCell.bits.writeUint(0, 1); // isComplete - dataCell.bits.writeUint(this.options.createdAt, 32); - dataCell.bits.writeAddress(this.options.marketplaceAddress); - dataCell.bits.writeAddress(this.options.nftItemAddress); - (dataCell.bits.writeAddress as WriteAddressMethod)(null); // nftOwnerAddress - dataCell.bits.writeCoins(this.options.fullPrice); - dataCell.refs.push(feesCell); - - return dataCell - } -} \ No newline at end of file diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTChangeOwnerModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTChangeOwnerModal.tsx deleted file mode 100644 index 8d8876ba0..000000000 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTChangeOwnerModal.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import { useDownloadCollectionMeta } from '../useDownloadCollectionMeta'; -import { useCopyText } from '$hooks/useCopyText'; -import { useInstance } from '$hooks/useInstance'; -import { useWallet } from '$hooks/useWallet'; -import { Highlight, Separator, Skeleton, Text } from '$uikit'; -import { NFTOperationFooter, useNFTOperationState } from '../NFTOperationFooter'; -import { NftChangeOwnerParams, TxRequestBody } from '../TXRequest.types'; -import { useUnlockVault } from '../useUnlockVault'; -import { NFTOperations } from '../NFTOperations'; -import * as S from '../NFTOperations.styles'; -import { toLocaleNumber } from '$utils'; -import { debugLog } from '$utils/debugLog'; -import { t } from '@tonkeeper/shared/i18n'; -import { Modal } from '@tonkeeper/uikit'; -import { Address } from '@tonkeeper/core'; - -type NFTChangeOwnerModalProps = TxRequestBody & { - redirectToActivity?: boolean; -}; - -export const NFTChangeOwnerModal = ({ - params, - redirectToActivity, - ...options -}: NFTChangeOwnerModalProps) => { - const meta = useDownloadCollectionMeta(params.nftCollectionAddress); - const { footerRef, onConfirm } = useNFTOperationState(options); - const [isShownDetails, setIsShownDetails] = React.useState(false); - const [fee, setFee] = React.useState(''); - const copyText = useCopyText(); - - const wallet = useWallet(); - const unlockVault = useUnlockVault(); - const operations = useInstance(() => { - return new NFTOperations(wallet); - }); - - const toggleDetails = React.useCallback(() => { - setIsShownDetails(!isShownDetails); - }, [isShownDetails]); - - React.useEffect(() => { - operations - .changeOwner(params) - .then((operation) => operation.estimateFee()) - .then((fee) => setFee(fee)) - .catch((err) => { - setFee('0.02'); - debugLog('[nft estimate fee]:', err); - }); - }, []); - - const handleConfirm = onConfirm(async ({ startLoading }) => { - const vault = await unlockVault(); - const privateKey = await vault.getTonPrivateKey(); - - startLoading(); - - const operation = await operations.changeOwner(params); - await operation.send(privateKey); - }); - - return ( - - - - - - - {meta.data?.image && } - - {meta.data?.name ?? '...'} - {t('nft_change_owner_title')} - - - copyText(params.newOwnerAddress)}> - - {t('nft_new_owner_address')} - - {Address.toShort(params.newOwnerAddress, 6)} - - - - - fee && copyText(toLocaleNumber(fee))}> - - {t('nft_fee')} - - {fee ? ( - {toLocaleNumber(fee)} TON - ) : ( - - )} - - - - - {isShownDetails && ( - - copyText(params.nftCollectionAddress)}> - - NFT collection ID - - {Address.toShort(params.nftCollectionAddress, 8)} - - - - - )} - - - - {isShownDetails ? t('nft_hide_details') : t('nft_show_details')} - - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTCollectionDeployModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTCollectionDeployModal.tsx deleted file mode 100644 index c260e39e9..000000000 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTCollectionDeployModal.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React from 'react'; -import { NFTCollectionMeta } from '../useDownloadCollectionMeta'; -import { useDownloadMetaFromUri } from '../useDownloadMetaFromUri'; -import { useCopyText } from '$hooks/useCopyText'; -import { useInstance } from '$hooks/useInstance'; -import { useWallet } from '$hooks/useWallet'; -import { Highlight, Separator, Skeleton, Text } from '$uikit'; -import { NFTOperationFooter, useNFTOperationState } from '../NFTOperationFooter'; -import { NftCollectionDeployParams, TxRequestBody } from '../TXRequest.types'; -import { useUnlockVault } from '../useUnlockVault'; -import { NFTOperations } from '../NFTOperations'; -import * as S from '../NFTOperations.styles'; -import { toLocaleNumber } from '$utils'; -import { debugLog } from '$utils/debugLog'; -import { t } from '@tonkeeper/shared/i18n'; -import { Modal } from '@tonkeeper/uikit'; -import { Address } from '@tonkeeper/core'; - -type NFTCollectionDeployModalProps = TxRequestBody & { - redirectToActivity?: boolean; -}; - -export const NFTCollectionDeployModal = ({ - params, - redirectToActivity, - ...options -}: NFTCollectionDeployModalProps) => { - const meta = useDownloadMetaFromUri(params.collectionContentUri); - const { footerRef, onConfirm } = useNFTOperationState(options); - const [isShownDetails, setIsShownDetails] = React.useState(false); - const [fee, setFee] = React.useState('~'); - const copyText = useCopyText(); - - const unlockVault = useUnlockVault(); - const wallet = useWallet(); - const operations = useInstance(() => { - return new NFTOperations(wallet); - }); - - const toggleDetails = React.useCallback(() => { - setIsShownDetails(!isShownDetails); - }, [isShownDetails]); - - React.useEffect(() => { - operations - .deployCollection(params) - .then((operation) => operation.estimateFee()) - .then((fee) => setFee(fee)) - .catch((err) => { - setFee('0.02'); - debugLog('[nft estimate fee]:', err); - }); - }, []); - - const handleConfirm = onConfirm(async ({ startLoading }) => { - const vault = await unlockVault(); - const privateKey = await vault.getTonPrivateKey(); - - startLoading(); - - const operation = await operations.deployCollection(params); - const deploy = await operation.send(privateKey); - - console.log('DEPLOY', deploy); - }); - - return ( - - - - - - - {meta.data?.image && } - - {t('nft_deploy_collection_title')} - - - copyText(meta.data?.name)}> - - {t('nft_collection_name')} - {meta.data?.name ?? '...'} - - - - copyText(params.royaltyAddress)}> - - {t('nft_royalty_address')} - - {Address.toShort(params.royaltyAddress, 6)} - - - - - copyText(String(params.royalty * 100))}> - - {t('nft_royalty')} - {params.royalty * 100}% - - - - copyText(toLocaleNumber(String(fee)))}> - - {t('nft_fee')} - {toLocaleNumber(fee)} TON - - - - {isShownDetails && ( - - copyText(params.royaltyAddress)}> - - NFT collection ID - - {Address.toShort(params.royaltyAddress, 8)} - - - - copyText(params.collectionContentUri)}> - - Collection content URI - - {params.collectionContentUri} - - - - copyText(params.nftItemContentBaseUri)}> - - NFT item content base URI - - {params.nftItemContentBaseUri} - - - - copyText(params.nftItemCodeHex)}> - - NFT item code HEX - - {Address.toShort(params.nftItemCodeHex, 8)} - - - - - )} - - - - {isShownDetails ? t('nft_hide_details') : t('nft_show_details')} - - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTItemDeployModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTItemDeployModal.tsx deleted file mode 100644 index 4e83c35c6..000000000 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTItemDeployModal.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react'; -import { useDownloadCollectionMeta } from '../useDownloadCollectionMeta'; -import { useDownloadMetaFromUri } from '../useDownloadMetaFromUri'; -import { useCopyText } from '$hooks/useCopyText'; -import { useInstance } from '$hooks/useInstance'; -import { useWallet } from '$hooks/useWallet'; -import { Highlight, Separator, Skeleton, Text } from '$uikit'; -import { NFTOperationFooter, useNFTOperationState } from '../NFTOperationFooter'; -import { NftItemDeployParams, TxRequestBody } from '../TXRequest.types'; -import { NFTItemMeta } from '../useDownloadNFT'; -import { useUnlockVault } from '../useUnlockVault'; -import { NFTOperations } from '../NFTOperations'; -import * as S from '../NFTOperations.styles'; -import { toLocaleNumber } from '$utils'; -import { debugLog } from '$utils/debugLog'; -import { t } from '@tonkeeper/shared/i18n'; -import { Modal } from '@tonkeeper/uikit'; -import { Address } from '@tonkeeper/core'; - -type NFTItemDeployModalProps = TxRequestBody & { - redirectToActivity?: boolean; -}; - -export const NFTItemDeployModal = ({ - params, - redirectToActivity, - ...options -}: NFTItemDeployModalProps) => { - const itemMeta = useDownloadMetaFromUri( - params.nftItemContentBaseUri + params.itemContentUri, - ); - const collectionMeta = useDownloadCollectionMeta(params.nftCollectionAddress); - const { footerRef, onConfirm } = useNFTOperationState(options); - const [isShownDetails, setIsShownDetails] = React.useState(false); - const [fee, setFee] = React.useState('~'); - const copyText = useCopyText(); - - const unlockVault = useUnlockVault(); - const wallet = useWallet(); - const operations = useInstance(() => { - return new NFTOperations(wallet); - }); - - const toggleDetails = React.useCallback(() => { - setIsShownDetails(!isShownDetails); - }, [isShownDetails]); - - React.useEffect(() => { - operations - .deployItem(params) - .then((operation) => operation.estimateFee()) - .then((fee) => setFee(fee)) - .catch((err) => { - setFee('0.02'); - debugLog('[nft estimate fee]:', err); - }); - }, []); - const handleConfirm = onConfirm(async ({ startLoading }) => { - const vault = await unlockVault(); - const privateKey = await vault.getTonPrivateKey(); - - startLoading(); - - const operation = await operations.deployItem(params); - const deploy = await operation.send(privateKey); - - console.log('DEPLOY', deploy); - }); - - return ( - - - - - - - {itemMeta.data?.image && } - - {t('nft_item_deploy_title')} - - - copyText(itemMeta.data?.name)}> - - {t('nft_item_name')} - {itemMeta.data?.name ?? '...'} - - - - copyText(collectionMeta.data?.name)}> - - {t('nft_collection')} - - {collectionMeta.data?.name ?? '...'} - - - - - copyText(toLocaleNumber(fee))}> - - {t('nft_fee')} - {toLocaleNumber(fee)} TON - - - - {isShownDetails && ( - - copyText(String(params.itemIndex))}> - - Item index - {params.itemIndex} - - - copyText(params.nftCollectionAddress)}> - - NFT collection ID - - {Address.toShort(params.nftCollectionAddress, 8)} - - - - copyText(params.itemContentUri)}> - - NFT item code HEX - {params.itemContentUri} - - - - )} - - - - {isShownDetails ? t('nft_hide_details') : t('nft_show_details')} - - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSaleCancelModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSaleCancelModal.tsx deleted file mode 100644 index 4fcbdebf2..000000000 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSaleCancelModal.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react'; -import { useCopyText } from '$hooks/useCopyText'; -import { useInstance } from '$hooks/useInstance'; -import { useWallet } from '$hooks/useWallet'; -import { Highlight, Icon, Skeleton, Text } from '$uikit'; -import { toLocaleNumber } from '$utils'; -import { debugLog } from '$utils/debugLog'; -import { NFTOperationFooter, useNFTOperationState } from '../NFTOperationFooter'; -import { NftSaleCancelParams, TxRequestBody } from '../TXRequest.types'; -import { useDownloadNFT } from '../useDownloadNFT'; -import { useUnlockVault } from '../useUnlockVault'; -import { NFTOperations } from '../NFTOperations'; -import * as S from '../NFTOperations.styles'; -import { t } from '@tonkeeper/shared/i18n'; -import { Modal } from '@tonkeeper/uikit'; - -type NFTSaleCancelModalProps = TxRequestBody & { - redirectToActivity?: boolean; -}; - -export const NFTSaleCancelModal = ({ - params, - redirectToActivity, - ...options -}: NFTSaleCancelModalProps) => { - const item = useDownloadNFT(params.nftItemAddress); - const { footerRef, onConfirm } = useNFTOperationState(options); - const [fee, setFee] = React.useState(''); - const copyText = useCopyText(); - - const unlockVault = useUnlockVault(); - const wallet = useWallet(); - const operations = useInstance(() => { - return new NFTOperations(wallet); - }); - - React.useEffect(() => { - operations - .saleCancel(params) - .then((operation) => operation.estimateFee()) - .then((fee) => setFee(fee)) - .catch((err) => { - setFee('0.02'); - debugLog('[nft estimate fee]:', err); - }); - }, []); - - const handleConfirm = onConfirm(async ({ startLoading }) => { - const vault = await unlockVault(); - const privateKey = await vault.getTonPrivateKey(); - - startLoading(); - - const operation = await operations.saleCancel(params); - const deploy = await operation.send(privateKey); - - console.log('DEPLOY', deploy); - }); - - const isTG = (item.data?.dns || item.data?.metadata?.name)?.endsWith('.t.me'); - const isDNS = !!item.data?.dns && !isTG; - - const caption = React.useMemo(() => { - let text = '...'; - if (item.data?.metadata) { - text = `${item.data.dns || item.data.metadata.name}`; - } - - if (item.data?.collection) { - text += ` · ${isDNS ? 'TON DNS' : item.data.collection.name}`; - } - - return item.data ? text : '...'; - }, [item.data]); - - return ( - - - - - - - - - - {caption} - {item.data?.approved_by?.length ? ( - - ) : null} - - {t('nft_sale_cancel_title')} - - - copyText(toLocaleNumber(fee))}> - - {t('nft_fee')} - - {fee ? ( - {toLocaleNumber(fee)} TON - ) : ( - - )} - - - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSalePlaceGetgemsModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSalePlaceGetgemsModal.tsx deleted file mode 100644 index fb02d77a5..000000000 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSalePlaceGetgemsModal.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import React from 'react'; -import BigNumber from 'bignumber.js'; -import { useCopyText } from '$hooks/useCopyText'; -import { useInstance } from '$hooks/useInstance'; -import { useWallet } from '$hooks/useWallet'; -import { Highlight, Icon, Separator, Skeleton, Text } from '$uikit'; -import { delay, retry, toLocaleNumber } from '$utils'; -import { debugLog } from '$utils/debugLog'; -import { NFTOperationFooter, useNFTOperationState } from '../NFTOperationFooter'; -import { NftSalePlaceGetgemsParams, TxRequestBody } from '../TXRequest.types'; -import { useDownloadNFT } from '../useDownloadNFT'; -import { useUnlockVault } from '../useUnlockVault'; -import { NFTOperations } from '../NFTOperations'; -import * as S from '../NFTOperations.styles'; -import { t } from '@tonkeeper/shared/i18n'; -import { Ton } from '$libs/Ton'; -import { Modal } from '@tonkeeper/uikit'; -import { Address } from '@tonkeeper/core'; - -type NFTSalePlaceModalProps = TxRequestBody & { - redirectToActivity?: boolean; -}; - -export const NFTSalePlaceGetgemsModal = ({ - params, - redirectToActivity, - ...options -}: NFTSalePlaceModalProps) => { - const item = useDownloadNFT(params.nftItemAddress); - const { footerRef, onConfirm } = useNFTOperationState(options); - const [isShownDetails, setIsShownDetails] = React.useState(false); - const [txfee, setTxFee] = React.useState(''); - const copyText = useCopyText(); - - const wallet = useWallet(); - const unlockVault = useUnlockVault(); - const operations = useInstance(() => { - return new NFTOperations(wallet); - }); - - const toggleDetails = React.useCallback(() => { - setIsShownDetails(!isShownDetails); - }, [isShownDetails]); - - React.useEffect(() => { - operations - .salePlaceGetGems(params) - .then((operation) => operation.estimateFee()) - .then((fee) => setTxFee(fee)) - .catch((err) => { - setTxFee('0.02'); - debugLog('[nft estimate fee]:', err); - }); - }, []); - - const transferToContract = React.useCallback( - async (contractAddress: string, secretKey: Uint8Array) => { - const info = await wallet!.ton.getWalletInfo(contractAddress); - - if (['empty', 'uninit', 'nonexist'].includes(info?.status ?? '')) { - throw new Error('Contract uninitialized'); - } - - const operationTransfer = await operations.transfer( - { - newOwnerAddress: contractAddress, - nftItemAddress: params.nftItemAddress, - forwardAmount: params.forwardAmount, - amount: params.transferAmount, - }, - { useCurrentWallet: true }, - ); - - const transfer = await operationTransfer.send(secretKey); - - console.log('transfer', transfer); - }, - [], - ); - - const handleConfirm = onConfirm(async ({ startLoading }) => { - const vault = await unlockVault(); - - startLoading(); - - const privateKey = await vault.getTonPrivateKey(); - const operation = await operations.salePlaceGetGems(params); - const deploy = await operation.send(privateKey); - console.log('deploy', deploy); - - const saleData = operation.getData(); - - await delay(15 * 1000); - const transfer = () => transferToContract(saleData.contractAddress, privateKey); - retry(transfer, { attempt: 5, delay: 5 * 1000 }).catch((err) => { - debugLog('[NFTSaleGetgems retry]', err); - }); - }); - - const fullPrice = React.useMemo(() => { - return Ton.fromNano(params.fullPrice); - }, []); - - const marketplaceFee = React.useMemo(() => { - return Ton.fromNano(params.marketplaceFee); - }, []); - - const royaltyAmount = React.useMemo(() => { - return Ton.fromNano(params.royaltyAmount); - }, []); - - const amount = React.useMemo(() => { - const deployAmount = Ton.fromNano(params.deployAmount); - const transferAmount = Ton.fromNano(params.transferAmount); - - return new BigNumber(deployAmount).plus(transferAmount).toString(); - }, []); - - const blockchainFee = React.useMemo(() => { - if (txfee !== '') { - return new BigNumber(txfee).plus(amount).toString(); - } - - return false; - }, [txfee]); - - const feeAndRoyalties = React.useMemo(() => { - if (txfee !== '') { - return new BigNumber(txfee) - .plus(amount) - .plus(marketplaceFee) - .plus(royaltyAmount) - .toString(); - } - - return false; - }, [txfee]); - - const proceeds = React.useMemo(() => { - if (feeAndRoyalties) { - return new BigNumber(fullPrice).minus(feeAndRoyalties).toString(); - } - - return false; - }, [fullPrice, feeAndRoyalties]); - - const isTG = (item.data?.dns || item.data?.metadata?.name)?.endsWith('.t.me'); - const isDNS = !!item.data?.dns && !isTG; - - const caption = React.useMemo(() => { - let text = '...'; - if (item.data?.metadata) { - text = `${item.data.dns || item.data.metadata.name}`; - } - - if (item.data?.collection) { - text += ` · ${isDNS ? 'TON DNS' : item.data.collection.name}`; - } - - return item.data ? text : '...'; - }, [item.data]); - - return ( - - - - - - - - - - {caption} - {item.data?.approved_by?.length ? ( - - ) : null} - - {t('nft_sale_place_title')} - - - copyText(params.marketplaceAddress)}> - - {t('nft_marketplace_address')} - - {Address.toShort(params.marketplaceAddress, 6)} - - - - - copyText(toLocaleNumber(fullPrice))}> - - {t('nft_price')} - {toLocaleNumber(fullPrice)} TON - - - - proceeds && copyText(toLocaleNumber(proceeds))}> - - {t('nft_proceeds')} - - {proceeds ? ( - {toLocaleNumber(proceeds)} TON - ) : ( - - )} - - - - - feeAndRoyalties && copyText(toLocaleNumber(feeAndRoyalties))} - > - - {t('nft_fee_and_royalties')} - - {feeAndRoyalties ? ( - {toLocaleNumber(feeAndRoyalties)} TON - ) : ( - - )} - - - - - {isShownDetails && ( - - copyText(params.nftItemAddress)}> - - NFT item ID - - {Address.toShort(params.nftItemAddress, 8)} - - - - copyText(toLocaleNumber(marketplaceFee))}> - - Marketplace fee - - {toLocaleNumber(marketplaceFee)} TON - - - - copyText(params.royaltyAddress)}> - - Royalty address - - {Address.toShort(params.royaltyAddress, 8)} - - - - copyText(toLocaleNumber(royaltyAmount))}> - - Royalty - {royaltyAmount ? ( - {toLocaleNumber(royaltyAmount)} TON - ) : ( - - )} - - - blockchainFee && copyText(toLocaleNumber(blockchainFee))} - > - - Blockchain fee - {blockchainFee ? ( - {toLocaleNumber(blockchainFee)} TON - ) : ( - - )} - - - - )} - - - - {isShownDetails ? t('nft_hide_details') : t('nft_show_details')} - - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSalePlaceModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSalePlaceModal.tsx deleted file mode 100644 index b12559033..000000000 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSalePlaceModal.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import React from 'react'; -import BigNumber from 'bignumber.js'; -import { useCopyText } from '$hooks/useCopyText'; -import { useInstance } from '$hooks/useInstance'; -import { useWallet } from '$hooks/useWallet'; -import { Highlight, Icon, Separator, Skeleton, Text } from '$uikit'; -import { toLocaleNumber } from '$utils'; -import { debugLog } from '$utils/debugLog'; -import { NFTOperationFooter, useNFTOperationState } from '../NFTOperationFooter'; -import { NftSalePlaceParams, TxRequestBody } from '../TXRequest.types'; -import { useDownloadNFT } from '../useDownloadNFT'; -import { useUnlockVault } from '../useUnlockVault'; -import { NFTOperations } from '../NFTOperations'; -import * as S from '../NFTOperations.styles'; -import { t } from '@tonkeeper/shared/i18n'; -import { Ton } from '$libs/Ton'; -import { Modal } from '@tonkeeper/uikit'; -import { Address } from '@tonkeeper/core'; - -type NFTSalePlaceModalProps = TxRequestBody & { - redirectToActivity?: boolean; -}; - -export const NFTSalePlaceModal = ({ - params, - redirectToActivity, - ...options -}: NFTSalePlaceModalProps) => { - const item = useDownloadNFT(params.nftItemAddress); - const { footerRef, onConfirm } = useNFTOperationState(options); - const [isShownDetails, setIsShownDetails] = React.useState(false); - const [txfee, setTxFee] = React.useState(''); - const copyText = useCopyText(); - - const wallet = useWallet(); - const unlockVault = useUnlockVault(); - const operations = useInstance(() => { - return new NFTOperations(wallet); - }); - - const toggleDetails = React.useCallback(() => { - setIsShownDetails(!isShownDetails); - }, [isShownDetails]); - - React.useEffect(() => { - operations - .salePlace(params) - .then((operation) => operation.estimateFee()) - .then((fee) => setTxFee(fee)) - .catch((err) => { - setTxFee('0.02'); - debugLog('[nft estimate fee]:', err); - }); - }, []); - - const handleConfirm = onConfirm(async ({ startLoading }) => { - const vault = await unlockVault(); - const privateKey = await vault.getTonPrivateKey(); - - startLoading(); - - const operation = await operations.salePlace(params); - const deploy = await operation.send(privateKey); - - console.log('DEPLOY', deploy); - }); - - const fullPrice = React.useMemo(() => { - return Ton.fromNano(params.fullPrice); - }, []); - - const marketplaceFee = React.useMemo(() => { - return Ton.fromNano(params.marketplaceFee); - }, []); - - const royaltyAmount = React.useMemo(() => { - return Ton.fromNano(params.royaltyAmount); - }, []); - - const amount = React.useMemo(() => { - return Ton.fromNano(params.amount); - }, []); - - const blockchainFee = React.useMemo(() => { - if (txfee !== '') { - return new BigNumber(txfee).plus(amount).toString(); - } - - return false; - }, [txfee]); - - const feeAndRoyalties = React.useMemo(() => { - if (txfee !== '') { - return new BigNumber(txfee) - .plus(amount) - .plus(marketplaceFee) - .plus(royaltyAmount) - .toString(); - } - - return false; - }, [txfee]); - - const proceeds = React.useMemo(() => { - if (feeAndRoyalties) { - return new BigNumber(fullPrice).minus(feeAndRoyalties).toString(); - } - - return false; - }, [fullPrice, feeAndRoyalties]); - - const isTG = (item.data?.dns || item.data?.metadata?.name)?.endsWith('.t.me'); - const isDNS = !!item.data?.dns && !isTG; - - const caption = React.useMemo(() => { - let text = '...'; - if (item.data?.metadata) { - text = `${item.data.dns || item.data.metadata.name}`; - } - - if (item.data?.collection) { - text += ` · ${isDNS ? 'TON DNS' : item.data.collection.name}`; - } - - return item.data ? text : '...'; - }, [item.data]); - - return ( - - - - - - - - - - {caption} - {item.data?.approved_by?.length ? ( - - ) : null} - - {t('nft_sale_place_title')} - - - copyText(params.marketplaceAddress)}> - - {t('nft_marketplace_address')} - - {Address.toShort(params.marketplaceAddress, 6)} - - - - - fullPrice && copyText(toLocaleNumber(fullPrice))}> - - {t('nft_price')} - - {fullPrice ? ( - {toLocaleNumber(fullPrice)} TON - ) : ( - - )} - - - - - proceeds && copyText(toLocaleNumber(proceeds))}> - - {t('nft_proceeds')} - - {proceeds ? ( - {toLocaleNumber(proceeds)} TON - ) : ( - - )} - - - - - feeAndRoyalties && copyText(toLocaleNumber(feeAndRoyalties))} - > - - {t('nft_fee_and_royalties')} - - {feeAndRoyalties ? ( - {toLocaleNumber(feeAndRoyalties)} TON - ) : ( - - )} - - - - - {isShownDetails && ( - - copyText(params.nftItemAddress)}> - - NFT item ID - - {Address.toShort(params.nftItemAddress, 8)} - - - - copyText(toLocaleNumber(marketplaceFee))}> - - Marketplace fee - - {toLocaleNumber(marketplaceFee)} TON - - - - copyText(params.royaltyAddress)}> - - Royalty address - - {Address.toShort(params.royaltyAddress, 8)} - - - - royaltyAmount && copyText(toLocaleNumber(royaltyAmount))} - > - - Royalty - {royaltyAmount ? ( - {toLocaleNumber(royaltyAmount)} TON - ) : ( - - )} - - - blockchainFee && copyText(toLocaleNumber(blockchainFee))} - > - - Blockchain fee - {blockchainFee ? ( - {toLocaleNumber(blockchainFee)} TON - ) : ( - - )} - - - - )} - - - - {isShownDetails ? t('nft_hide_details') : t('nft_show_details')} - - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSingleDeployModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSingleDeployModal.tsx deleted file mode 100644 index 621a11f29..000000000 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTSingleDeployModal.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; -import { useCopyText } from '$hooks/useCopyText'; -import { useInstance } from '$hooks/useInstance'; -import { useWallet } from '$hooks/useWallet'; -import { Highlight, Separator, Skeleton, Text } from '$uikit'; -import { NFTOperationFooter, useNFTOperationState } from '../NFTOperationFooter'; -import { NftSingleDeployParams, TxRequestBody } from '../TXRequest.types'; -import { useDownloadMetaFromUri } from '../useDownloadMetaFromUri'; -import { NFTItemMeta } from '../useDownloadNFT'; -import { useUnlockVault } from '../useUnlockVault'; -import { NFTOperations } from '../NFTOperations'; -import * as S from '../NFTOperations.styles'; -import { toLocaleNumber } from '$utils'; -import { debugLog } from '$utils/debugLog'; -import { t } from '@tonkeeper/shared/i18n'; -import { Modal } from '@tonkeeper/uikit'; - -type NFTSingleDeployModal = TxRequestBody & { - redirectToActivity?: boolean; -}; - -export const NFTSingleDeployModal = ({ - params, - redirectToActivity, - ...options -}: NFTSingleDeployModal) => { - const itemMeta = useDownloadMetaFromUri(params.itemContentUri); - const { footerRef, onConfirm } = useNFTOperationState(options); - const [fee, setFee] = React.useState(''); - const copyText = useCopyText(); - - const wallet = useWallet(); - const unlockVault = useUnlockVault(); - const operations = useInstance(() => { - return new NFTOperations(wallet); - }); - - React.useEffect(() => { - operations - .deploy({ - stateInitHex: params.stateInitHex, - address: params.contractAddress, - amount: params.amount, - }) - .then((operation) => operation.estimateFee()) - .then((fee) => setFee(fee)) - .catch((err) => { - setFee('0.02'); - debugLog('[nft estimate fee]:', err); - }); - }, []); - - const handleConfirm = onConfirm(async ({ startLoading }) => { - const vault = await unlockVault(); - - startLoading(); - - const privateKey = await vault.getTonPrivateKey(); - const operation = await operations.deploy({ - stateInitHex: params.stateInitHex, - address: params.contractAddress, - amount: params.amount, - }); - - await operation.send(privateKey); - }); - - return ( - - - - - - - {itemMeta.data?.image && ( - - )} - - {t('nft_item_deploy_title')} - - - copyText(itemMeta.data?.name)}> - - {t('nft_item_name')} - - {itemMeta.data?.name ? ( - {itemMeta.data.name} - ) : ( - - )} - - - - - fee && copyText(toLocaleNumber(fee))}> - - {t('nft_fee')} - - {fee ? ( - {toLocaleNumber(fee)} TON - ) : ( - - )} - - - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTTransferModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTTransferModal.tsx deleted file mode 100644 index 8491b3bff..000000000 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/NFTTransferModal.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import React from 'react'; -import { useCopyText } from '$hooks/useCopyText'; -import { useInstance } from '$hooks/useInstance'; -import { useWallet } from '$hooks/useWallet'; -import { Highlight, Icon, Separator, Skeleton, Text } from '$uikit'; -import { NFTOperationFooter, useNFTOperationState } from '../NFTOperationFooter'; -import { useDownloadCollectionMeta } from '../useDownloadCollectionMeta'; -import { NftTransferParams, TxRequestBody } from '../TXRequest.types'; -import { useDownloadNFT } from '../useDownloadNFT'; -import { useUnlockVault } from '../useUnlockVault'; -import { NFTOperations } from '../NFTOperations'; -import * as S from '../NFTOperations.styles'; -import { toLocaleNumber } from '$utils'; -import { debugLog } from '$utils/debugLog'; -import { t } from '@tonkeeper/shared/i18n'; -import { CryptoCurrencies } from '$shared/constants'; -import { useDispatch } from 'react-redux'; -import { nftsActions } from '$store/nfts'; -import { Modal } from '@tonkeeper/uikit'; -import { formatter } from '$utils/formatter'; -import { goBack, push } from '$navigation/imperative'; -import { SheetActions } from '@tonkeeper/router'; -import { Ton } from '$libs/Ton'; -import { walletWalletSelector } from '$store/wallet'; -import { store, Toast } from '$store'; -import { - checkIsInsufficient, - openInsufficientFundsModal, -} from '$core/ModalContainer/InsufficientFunds/InsufficientFunds'; -import { Address } from '@tonkeeper/core'; - -type NFTTransferModalProps = TxRequestBody & { - redirectToActivity?: boolean; -}; - -export const NFTTransferModal = ({ - params, - fee: precalculatedFee, - redirectToActivity, - ...options -}: NFTTransferModalProps) => { - const { footerRef, onConfirm } = useNFTOperationState(options); - const item = useDownloadNFT(params.nftItemAddress); - const collectionMeta = useDownloadCollectionMeta(); - const copyText = useCopyText(); - const dispatch = useDispatch(); - - const [isShownDetails, setIsShownDetails] = React.useState(false); - const [fee, setFee] = React.useState(precalculatedFee ?? ''); - - const wallet = useWallet(); - const unlockVault = useUnlockVault(); - const operations = useInstance(() => { - return new NFTOperations(wallet); - }); - - const toggleDetails = React.useCallback(() => { - setIsShownDetails(!isShownDetails); - }, [isShownDetails]); - - React.useEffect(() => { - operations - .getCollectionAddressByItem(params.nftItemAddress) - .then((address) => { - collectionMeta.download(address); - }) - .catch((err) => debugLog('[NFT getCollectionUriByItem]', err)); - - if (!precalculatedFee) { - operations - .transfer(params) - .then((operation) => operation.estimateFee()) - .then((fee) => setFee(fee)) - .catch((err) => debugLog('[nft estimate fee]:', err)); - } - }, [precalculatedFee]); - - const handleConfirm = onConfirm(async ({ startLoading }) => { - const vault = await unlockVault(); - const privateKey = await vault.getTonPrivateKey(); - - startLoading(); - - const operation = await operations.transfer(params); - const deploy = await operation.send(privateKey); - - const txCurrency = CryptoCurrencies.Ton; - dispatch( - nftsActions.removeFromBalances({ - nftKey: `${txCurrency}_${params.nftItemAddress}`, - }), - ); - console.log('DEPLOY', deploy); - }); - - const isTG = (item.data?.dns || item.data?.metadata?.name)?.endsWith('.t.me'); - const isDNS = !!item.data?.dns && !isTG; - - const caption = React.useMemo(() => { - let text = '...'; - if (item.data?.metadata) { - text = `${(!isTG && item.data.dns) || item.data.metadata.name}`; - } - - if (collectionMeta.data) { - text += ` · ${isDNS ? 'TON DNS' : collectionMeta.data.name}`; - } - - return item.data ? text : '...'; - }, [item.data, collectionMeta.data, isDNS, isTG]); - - return ( - - - - - - - - - - {caption} - {item.data?.approved_by?.length ? ( - - ) : null} - - {t('nft_transfer_title')} - - - copyText(params.newOwnerAddress)}> - - {t('nft_transfer_recipient')} - - {Address.toShort(params.newOwnerAddress, 6)} - - - - - fee && copyText(toLocaleNumber(fee))}> - - - {parseFloat(fee) >= 0 ? t('transaction_fee') : t('transaction_refund')} - - - {fee ? ( - - {formatter.format(fee, { - currencySeparator: 'wide', - currency: CryptoCurrencies.Ton.toLocaleUpperCase(), - absolute: true, - })} - - ) : ( - - )} - - - - - {isShownDetails && ( - - copyText(collectionMeta.data?.name)}> - - NFT collection ID - - {collectionMeta.data?.name ?? '...'} - - - - copyText(params.nftItemAddress)}> - - NFT item ID - - {Address.toShort(params.nftItemAddress, 8)} - - - - - )} - - - - {isShownDetails ? t('nft_hide_details') : t('nft_show_details')} - - - - - - - - - - ); -}; - -export const openNFTTransfer = async (params: NFTTransferModalProps) => { - push('SheetsProvider', { - $$action: SheetActions.ADD, - component: NFTTransferModal, - path: 'NFTTransfer', - params, - }); - - return true; -}; - -export async function checkFundsAndOpenNFTTransfer( - nftAddress: string, - newOwnerAddress: string, -) { - const transferParams = { - newOwnerAddress, - nftItemAddress: nftAddress, - amount: Ton.toNano('1'), - forwardAmount: '1', - }; - - const wallet = walletWalletSelector(store.getState()); - - if (!wallet) { - console.log('no wallet'); - return; - } - - Toast.loading(); - - let fee = '0'; - try { - const operations = new NFTOperations(wallet); - fee = await operations - .transfer(transferParams as any) - .then((operation) => operation.estimateFee()); - } catch (e) {} - - // compare balance and transfer amount, because transfer will fail - if (fee === '0') { - const checkResult = await checkIsInsufficient(transferParams.amount); - if (checkResult.insufficient) { - Toast.hide(); - return openInsufficientFundsModal({ - totalAmount: transferParams.amount, - balance: checkResult.balance, - }); - } - } - - if (parseFloat(fee) < 0) { - transferParams.amount = Ton.toNano('0.05'); - } else { - transferParams.amount = Ton.toNano(fee).add(Ton.toNano('0.01')); - } - - Toast.hide(); - - openNFTTransfer({ - type: 'nft-transfer', - // expires in 100 minutes - expires_sec: Date.now() / 1000 + 6000, - response_options: {} as any, - params: transferParams, - fee, - }); -} diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx index 3a1588883..a9c5b6581 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx @@ -2,16 +2,11 @@ import React, { memo, useEffect, useMemo } from 'react'; import { NFTOperationFooter, useNFTOperationState } from '../NFTOperationFooter'; import { SignRawParams, TxBodyOptions } from '../TXRequest.types'; import { useUnlockVault } from '../useUnlockVault'; -import { NFTOperations } from '../NFTOperations'; -import { - calculateActionsTotalAmount, - calculateMessageTransferAmount, - delay, -} from '$utils'; +import { calculateMessageTransferAmount, delay } from '$utils'; import { debugLog } from '$utils/debugLog'; import { t } from '@tonkeeper/shared/i18n'; import { store, Toast } from '$store'; -import { Modal, View, Text, Steezy, List } from '@tonkeeper/uikit'; +import { List, Modal, Steezy, Text, View } from '@tonkeeper/uikit'; import { push } from '$navigation/imperative'; import { SheetActions } from '@tonkeeper/router'; import { @@ -29,15 +24,21 @@ import { ActivityModel, Address, AnyActionItem, + ContractService, + contractVersionsMap, + TransactionService, } from '@tonkeeper/core'; import { ActionListItemByType } from '@tonkeeper/shared/components/ActivityList/ActionListItemByType'; import { useSelector } from 'react-redux'; import { fiatCurrencySelector } from '$store/main'; import { useGetTokenPrice } from '$hooks/useTokenPrice'; import { formatValue, getActionTitle } from '@tonkeeper/shared/utils/signRaw'; +import { Buffer } from 'buffer'; +import { trackEvent } from '$utils/stats'; +import { Events, SendAnalyticsFrom } from '$store/models'; +import { getWalletSeqno } from '@tonkeeper/shared/utils/wallet'; interface SignRawModalProps { - action: Awaited>; consequences?: MessageConsequences; options: TxBodyOptions; params: SignRawParams; @@ -47,15 +48,8 @@ interface SignRawModalProps { } export const SignRawModal = memo((props) => { - const { - options, - params, - action, - onSuccess, - onDismiss, - consequences, - redirectToActivity, - } = props; + const { options, params, onSuccess, onDismiss, consequences, redirectToActivity } = + props; const { footerRef, onConfirm } = useNFTOperationState(options); const unlockVault = useUnlockVault(); @@ -68,12 +62,29 @@ export const SignRawModal = memo((props) => { startLoading(); - await action.send(privateKey, async (boc) => { - if (onSuccess) { - await delay(1750); - onSuccess(boc); - } + const contract = ContractService.getWalletContract( + contractVersionsMap[vault.getVersion() ?? 'v4R2'], + Buffer.from(vault.tonPublicKey), + ); + const boc = TransactionService.createTransfer(contract, { + messages: TransactionService.parseSignRawMessages(params.messages), + seqno: await getWalletSeqno(), + sendMode: 3, + secretKey: Buffer.from(privateKey), }); + + await tonapi.blockchain.sendBlockchainMessage( + { + boc, + }, + { format: 'text' }, + ); + + if (onSuccess) { + trackEvent(Events.SendSuccess, { from: SendAnalyticsFrom.SignRaw }); + await delay(1750); + onSuccess(boc); + } }); useEffect(() => { @@ -225,12 +236,27 @@ export const openSignRawModal = async ( await TonConnectRemoteBridge.closeOtherTransactions(); } - const operations = new NFTOperations(wallet); - const action = await operations.signRaw(params); + const contract = ContractService.getWalletContract( + contractVersionsMap[wallet.ton.version], + Buffer.from(wallet.ton.vault.tonPublicKey), + ); + + // in case of error we should check current TON balance and show "insufficient funds" modal + const totalAmount = calculateMessageTransferAmount(params.messages); + const checkResult = await checkIsInsufficient(totalAmount); + if (checkResult.insufficient) { + Toast.hide(); + onDismiss?.(); + return openInsufficientFundsModal({ totalAmount, balance: checkResult.balance }); + } let consequences: MessageConsequences | null = null; try { - const boc = await action.getBoc(); + const boc = TransactionService.createTransfer(contract, { + messages: TransactionService.parseSignRawMessages(params.messages), + seqno: await getWalletSeqno(), + secretKey: Buffer.alloc(64), + }); consequences = await tonapi.wallet.emulateMessageToWallet({ boc }); Toast.hide(); @@ -241,28 +267,9 @@ export const openSignRawModal = async ( const tonapiError = err?.response?.data?.error; const errorMessage = tonapiError ?? `no response; status code: ${err.status};`; - // in case of error we should check current TON balance and show "insufficient funds" modal - const totalAmount = calculateMessageTransferAmount(params.messages); - const checkResult = await checkIsInsufficient(totalAmount); - if (checkResult.insufficient) { - Toast.hide(); - onDismiss?.(); - return openInsufficientFundsModal({ totalAmount, balance: checkResult.balance }); - } - Toast.fail(`Emulation error: ${errorMessage}`, { duration: 5000 }); } - if (params.messages) { - const totalAmount = calculateActionsTotalAmount(params.messages); - const checkResult = await checkIsInsufficient(totalAmount); - if (checkResult.insufficient) { - Toast.hide(); - onDismiss?.(); - return openInsufficientFundsModal({ totalAmount, balance: checkResult.balance }); - } - } - push('SheetsProvider', { $$action: SheetActions.ADD, component: SignRawModal, @@ -270,7 +277,6 @@ export const openSignRawModal = async ( consequences, options, params, - action, onSuccess, onDismiss, redirectToActivity, diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts index f7bf7254e..76e179580 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts @@ -2,35 +2,22 @@ import { TransferMethodParams, WalletContract, } from 'tonweb/dist/types/contract/wallet/wallet-contract'; -import { - DeployParams, - NftChangeOwnerParams, - NftCollectionDeployParams, - NftItemDeployParams, - NftSaleCancelParams, - NftSalePlaceGetgemsParams, - NftSalePlaceParams, - NftTransferParams, - SignRawParams, -} from './TXRequest.types'; +import { DeployParams, NftTransferParams, SignRawParams } from './TXRequest.types'; import TonWeb, { Method } from 'tonweb'; import BigNumber from 'bignumber.js'; import { Base64, truncateDecimal } from '$utils'; import { Wallet } from 'blockchain'; import { NFTOperationError } from './NFTOperationError'; -import { GetGemsSaleContract } from './GetGemsSaleContract'; -import { Cell } from 'tonweb/dist/types/boc/cell'; import { Address as AddressType } from 'tonweb/dist/types/utils/address'; -import { Address } from 'ton-core'; +import { Address } from '@ton/core'; import { t } from '@tonkeeper/shared/i18n'; import { Ton } from '$libs/Ton'; import { getServerConfig } from '$shared/constants'; import { SendApi, Configuration as ConfigurationV1 } from 'tonapi-sdk-js'; -import axios from 'axios'; import { Tonapi } from '$libs/Tonapi'; -import { AccountEvent, Configuration, NFTApi } from '@tonkeeper/core/src/legacy'; +import { Configuration, NFTApi } from '@tonkeeper/core/src/legacy'; -const { NftCollection, NftItem, NftSale } = TonWeb.token.nft; +const { NftItem } = TonWeb.token.nft; type EstimateFeeTransferMethod = ( params: Omit, @@ -74,95 +61,6 @@ export class NFTOperations { this.getMyAddresses(); } - public async deployCollection(params: NftCollectionDeployParams) { - const wallet = params.ownerAddress - ? await this.getWalletByAddress(params.ownerAddress) - : this.getCurrentWallet(); - - const ownerAddress = params.ownerAddress - ? new TonWeb.utils.Address(params.ownerAddress) - : await wallet.getAddress(); - - const amount = Ton.fromNano(params.amount); - const seqno = await this.getSeqno(ownerAddress.toString(false)); - - let stateInit: Cell; - let nftCollectionAddress: string; - if (params.nftCollectionStateInitHex && params.contractAddress) { - stateInit = TonWeb.boc.Cell.oneFromBoc(params.nftCollectionStateInitHex); - nftCollectionAddress = params.contractAddress; - } else { - const royaltyAddress = new TonWeb.utils.Address(params.royaltyAddress); - - const collectionCodeCell = params.nftCollectionCodeHex - ? TonWeb.boc.Cell.oneFromBoc(params.nftCollectionCodeHex) - : undefined; - - const nftCollection = new NftCollection(wallet.provider, { - nftItemContentBaseUri: params.nftItemContentBaseUri, - collectionContentUri: params.collectionContentUri, - nftItemCodeHex: params.nftItemCodeHex, - royalty: params.royalty, - royaltyAddress, - ownerAddress, - code: collectionCodeCell, - }); - - const nftCollectionAddressInfo = await nftCollection.getAddress(); - nftCollectionAddress = nftCollectionAddressInfo.toString(true, true, true); - - const createdStateInit = await nftCollection.createStateInit(); - stateInit = createdStateInit.stateInit; - } - - return this.methods(wallet, { - amount: Ton.toNano(amount), - toAddress: nftCollectionAddress, - sendMode: 3, - stateInit, - seqno, - }); - } - - public async deployItem(params: NftItemDeployParams) { - const wallet = params.ownerAddress - ? await this.getWalletByAddress(params.ownerAddress) - : this.getCurrentWallet(); - - const ownerAddress = params.ownerAddress - ? new TonWeb.utils.Address(params.ownerAddress) - : await wallet.getAddress(); - - const seqno = await this.getSeqno(ownerAddress.toString(false)); - - const amount = this.toNano(params.amount); - const forwardAmount = this.toNano(params.forwardAmount); - - const nftCollectionAddressInfo = new TonWeb.utils.Address( - params.nftCollectionAddress, - ); - const nftCollectionAddress = nftCollectionAddressInfo.toString(true, true, true); - - const nftCollection = new NftCollection(wallet.provider, { - address: params.nftCollectionAddress, - }); - - const payload = nftCollection.createMintBody({ - itemContentUri: params.itemContentUri, - itemOwnerAddress: ownerAddress, - itemIndex: params.itemIndex, - amount: forwardAmount, - }); - - return this.methods(wallet, { - toAddress: nftCollectionAddress, - sendMode: 3, - payload, - amount, - seqno, - }); - } - public async transfer( params: Omit, options?: { useCurrentWallet?: boolean }, @@ -202,134 +100,6 @@ export class NFTOperations { }); } - public async changeOwner(params: NftChangeOwnerParams) { - const ownerAddress = await this.getOwnerAddressByCollection( - params.nftCollectionAddress, - ); - const wallet = await this.getWalletByAddress(ownerAddress); - const seqno = await this.getSeqno(ownerAddress); - - const amount = this.toNano(params.amount); - - const nftCollection = new NftCollection(wallet.provider, {}); - const nftCollectionAddressInfo = new TonWeb.utils.Address( - params.nftCollectionAddress, - ); - const nftCollectionAddress = nftCollectionAddressInfo.toString(true, true, true); - - const payload = nftCollection.createChangeOwnerBody({ - newOwnerAddress: new TonWeb.utils.Address(params.newOwnerAddress), - }); - - return this.methods(wallet, { - toAddress: nftCollectionAddress, - amount: amount, - sendMode: 3, - payload, - seqno, - }); - } - - public async saleCancel(params: NftSaleCancelParams) { - const wallet = await this.getWalletByAddress(params.ownerAddress); - - const saleAddress = new TonWeb.utils.Address(params.saleAddress); - const sale = new NftSale(wallet.provider, {}); - const payload = await sale.createCancelBody({}); - const amount = this.toNano(params.amount); - const seqno = await this.getSeqno(params.ownerAddress); - - return this.methods(wallet, { - toAddress: saleAddress, - amount: amount, - sendMode: 3, - payload, - seqno, - }); - } - - public async salePlace(params: NftSalePlaceParams) { - const ownerAddress = await this.getOwnerAddressByItem(params.nftItemAddress); - const wallet = await this.getWalletByAddress(ownerAddress); - - const marketplaceAddress = new TonWeb.utils.Address(params.marketplaceAddress); - const royaltyAddress = new TonWeb.utils.Address(params.royaltyAddress); - const nftItemAddress = new TonWeb.utils.Address(params.nftItemAddress); - - const sale = new NftSale(wallet.provider, { - marketplaceFee: this.toNano(params.marketplaceFee), - royaltyAmount: this.toNano(params.royaltyAmount), - fullPrice: this.toNano(params.fullPrice), - nftAddress: nftItemAddress, - marketplaceAddress, - royaltyAddress, - }); - - const createdStateInit = await sale.createStateInit(); - const amount = this.toNano(params.amount); - const seqno = await this.getSeqno(ownerAddress); - - const body = new TonWeb.boc.Cell(); - body.bits.writeUint(1, 32); // OP deploy new auction - body.bits.writeCoins(amount); - body.refs.push(createdStateInit.stateInit); - body.refs.push(new TonWeb.boc.Cell()); - - return this.methods(wallet, { - toAddress: marketplaceAddress, - amount: amount, - payload: body, - sendMode: 3, - seqno, - }); - } - - public async salePlaceGetGems(params: NftSalePlaceGetgemsParams) { - const wallet = this.getCurrentWallet(); - const amount = this.toNano(params.deployAmount); - const seqno = await this.getSeqno((await wallet.getAddress()).toString(false)); - - if (Number(params.forwardAmount) < 1) { - throw new NFTOperationError('forwardAmount must be greater than 0'); - } - - const getgems = new GetGemsSaleContract(this.tonwebWallet.provider, { - marketplaceFeeAddress: new TonWeb.utils.Address(params.marketplaceFeeAddress), - marketplaceAddress: new TonWeb.utils.Address(params.marketplaceAddress), - royaltyAddress: new TonWeb.utils.Address(params.royaltyAddress), - nftItemAddress: new TonWeb.utils.Address(params.nftItemAddress), - marketplaceFee: this.toNano(params.marketplaceFee), - royaltyAmount: this.toNano(params.royaltyAmount), - fullPrice: this.toNano(params.fullPrice), - createdAt: params.createdAt, - }); - - const { stateInit, address } = await getgems.createStateInit(); - const contractAddress = address.toString(true, true, true); - - const saleMessageBody = new TonWeb.boc.Cell(); - - let signature = TonWeb.utils.hexToBytes(params.marketplaceSignatureHex); - - let payload = new TonWeb.boc.Cell(); - payload.bits.writeUint(1, 32); - payload.bits.writeBytes(signature); - payload.refs.push(stateInit); - payload.refs.push(saleMessageBody); - - return this.methods( - wallet, - { - toAddress: params.marketplaceAddress, - sendMode: 3, - amount, - payload, - seqno, - }, - { contractAddress }, - ); - } - public async deploy(params: DeployParams) { const wallet = this.getCurrentWallet(); const seqno = await this.getSeqno((await wallet.getAddress()).toString(false)); @@ -353,20 +123,6 @@ export class NFTOperations { }); } - // - // Info methods - // - - public async getCollectionUri(nftCollectionAddress: string) { - const nftCollection = new NftCollection(this.tonwebWallet.provider, { - address: nftCollectionAddress, - }); - - const data = await nftCollection.getCollectionData(); - - return data.collectionContentUri; - } - private seeIfBounceable(address: string) { try { return Address.isFriendly(address) @@ -444,20 +200,6 @@ export class NFTOperations { }; } - public async getCollectionAddressByItem(nftItemAddress: string) { - const nftItemData = await this.nftApi.getNftItemsByAddresses({ - getAccountsRequest: { accountIds: [nftItemAddress] }, - }); - - const collectionAddress = nftItemData.nftItems[0]?.collection?.address; - - if (!collectionAddress) { - throw new NFTOperationError('collectionAddress empty'); - } - const isTestnet = this.wallet.ton.isTestnet; - return new TonWeb.Address(collectionAddress).toString(true, true, true, isTestnet); - } - private async getOwnerAddressByItem(nftItemAddress: string) { const nftItemData = await this.nftApi.getNftItemsByAddresses({ getAccountsRequest: { accountIds: [nftItemAddress] }, @@ -596,4 +338,4 @@ export class NFTOperations { private getCurrentWallet(): WalletContract { return this.wallet.vault.tonWallet; } -} \ No newline at end of file +} diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types.ts b/packages/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types.ts index 28a655eb5..6ad3871d4 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types.ts +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types.ts @@ -1,3 +1,5 @@ +import { MessageRelaxed } from '@ton/core'; + export type NftCollectionDeployParams = { ownerAddress?: string; royaltyAddress: string; @@ -92,8 +94,8 @@ export interface SignRawMessage { } export type SignRawParams = { - source: string; - valid_until: number; + source?: string; + valid_until?: number; messages: SignRawMessage[]; }; @@ -134,8 +136,8 @@ export type TxResponseOptions = { export type TxRequestBody = { type: TxTypes; - expires_sec: number; - response_options: TxResponseOptions; + expires_sec?: number; + response_options?: TxResponseOptions; params: TParams; fee?: string; }; diff --git a/packages/mobile/src/core/ModalContainer/NFTTransferInputAddressModal/NFTTransferInputAddressModal.tsx b/packages/mobile/src/core/ModalContainer/NFTTransferInputAddressModal/NFTTransferInputAddressModal.tsx index c7a99b23d..e311f1ec5 100644 --- a/packages/mobile/src/core/ModalContainer/NFTTransferInputAddressModal/NFTTransferInputAddressModal.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTTransferInputAddressModal/NFTTransferInputAddressModal.tsx @@ -14,10 +14,16 @@ import TonWeb from 'tonweb'; import { Toast } from '$store'; import { useAnimatedStyle, withTiming } from 'react-native-reanimated'; import { openScanQR } from '$navigation'; -import { checkFundsAndOpenNFTTransfer } from '$core/ModalContainer/NFTOperations/Modals/NFTTransferModal'; import { SheetActions } from '@tonkeeper/router'; import { push } from '$navigation/imperative'; -import { Address } from '@tonkeeper/core'; +import { + Address, + AmountFormatter, + ContractService, + TransactionService, +} from '@tonkeeper/core'; +import { openSignRawModal } from '$core/ModalContainer/NFTOperations/Modals/SignRawModal'; +import { tk } from '@tonkeeper/shared/tonkeeper'; export const NFTTransferInputAddressModal = memo( ({ nftAddress }) => { @@ -39,7 +45,24 @@ export const NFTTransferInputAddressModal = memo { - await checkFundsAndOpenNFTTransfer(nftAddress, address); + await openSignRawModal( + { + messages: [ + { + amount: AmountFormatter.toNano(1), + address: nftAddress, + payload: ContractService.createNftTransferBody({ + queryId: Date.now(), + newOwnerAddress: address, + excessesAddress: tk.wallet.address.ton.raw, + }) + .toBoc() + .toString('base64'), + }, + ], + }, + {}, + ); }, [address, nftAddress]); // todo: move logic to separated hook diff --git a/packages/mobile/src/core/NFT/RenewDomainButton.tsx b/packages/mobile/src/core/NFT/RenewDomainButton.tsx index ac3e11cc8..a642da418 100644 --- a/packages/mobile/src/core/NFT/RenewDomainButton.tsx +++ b/packages/mobile/src/core/NFT/RenewDomainButton.tsx @@ -1,9 +1,4 @@ -import { - forwardRef, - useCallback, - useImperativeHandle, - useState, -} from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useState } from 'react'; import { View, StyleSheet } from 'react-native'; import { Button } from '$uikit/Button/Button'; import { Text } from '$uikit/Text/Text'; @@ -34,7 +29,8 @@ interface RenewDomainButtonProps { onSend: () => void; } -export const RenewDomainButton = forwardRef((props, ref) => { +export const RenewDomainButton = forwardRef( + (props, ref) => { const { disabled, expiringAt, loading, onSend, domainAddress, ownerAddress } = props; const [isPending, setIsPending] = useState(false); const wallet = useWallet(); @@ -53,7 +49,7 @@ export const RenewDomainButton = forwardRef ); - }); - + }, +); const styles = StyleSheet.create({ container: { diff --git a/packages/mobile/src/core/Send/Send.tsx b/packages/mobile/src/core/Send/Send.tsx index c672033c7..aace6c784 100644 --- a/packages/mobile/src/core/Send/Send.tsx +++ b/packages/mobile/src/core/Send/Send.tsx @@ -66,6 +66,7 @@ export const Send: FC = ({ route }) => { isInactive: initialIsInactive = false, from, expiryTimestamp, + redirectToActivity = true, } = route.params; const initialAddress = @@ -485,6 +486,7 @@ export const Send: FC = ({ route }) => { comment={comment} isCommentEncrypted={isCommentEncrypted} onConfirm={handleSend} + redirectToActivity={redirectToActivity} {...stepProps} /> )} diff --git a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.interface.ts b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.interface.ts index 5de850d38..dda7baa11 100644 --- a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.interface.ts +++ b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.interface.ts @@ -23,4 +23,5 @@ export interface ConfirmStepProps { isCommentEncrypted: boolean; onConfirm: () => Promise; isPreparing: boolean; + redirectToActivity: boolean; } diff --git a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx index ecbecdacb..a0c143e53 100644 --- a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx +++ b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx @@ -40,6 +40,7 @@ const ConfirmStepComponent: FC = (props) => { comment, isCommentEncrypted, onConfirm: sendTx, + redirectToActivity, } = props; const { footerRef, onConfirm } = useActionFooter(); @@ -348,6 +349,7 @@ const ConfirmStepComponent: FC = (props) => { withCloseButton={false} confirmTitle={t('confirm_sending_submit')} onPressConfirm={handleConfirm} + redirectToActivity={redirectToActivity} ref={footerRef} /> diff --git a/packages/mobile/src/core/Settings/Settings.tsx b/packages/mobile/src/core/Settings/Settings.tsx index c2673919e..605ad8018 100644 --- a/packages/mobile/src/core/Settings/Settings.tsx +++ b/packages/mobile/src/core/Settings/Settings.tsx @@ -167,6 +167,21 @@ export const Settings: FC = () => { [dispatch], ); + const handleSwitchLanguage = useCallback(() => { + Alert.alert(t('language.language_alert.title'), undefined, [ + { + text: t('language.language_alert.cancel'), + style: 'cancel', + }, + { + text: t('language.language_alert.open'), + onPress: () => { + Linking.openSettings(); + }, + }, + ]); + }, []); + const handleDevMenu = useCallback(() => { openDevMenu(); }, []); @@ -334,6 +349,15 @@ export const Settings: FC = () => { title={t('settings_search_engine')} /> + + {t('language.list_item.value')} + + } + title={t('language.list_item.title')} + /> {!!wallet && ( = () => { openDAppBrowser(getServerConfig('stakingInfoUrl')); }, []); + const otherPoolsEstimation = useMemo(() => { + const otherPools = activePools.filter( + (pool) => pool.implementation !== PoolImplementationType.LiquidTF, + ); + + return otherPools.reduce( + (acc, pool) => { + return { + balance: new BigNumber(acc.balance) + .plus(pool.balance || '0') + .decimalPlaces(Decimals[CryptoCurrencies.Ton]) + .toString(), + estimatedProfit: new BigNumber(pool.balance || '0') + .multipliedBy(new BigNumber(pool.apy).dividedBy(100)) + .plus(acc.estimatedProfit) + .decimalPlaces(Decimals[CryptoCurrencies.Ton]) + .toString(), + }; + }, + { balance: '0', estimatedProfit: '0' }, + ); + }, [activePools]); + const getEstimateProfitMessage = useCallback( (provider: StakingProvider) => { + if (new BigNumber(otherPoolsEstimation.balance).isGreaterThan(0)) { + const estimatedProfit = new BigNumber(otherPoolsEstimation.balance).multipliedBy( + new BigNumber(provider.maxApy).dividedBy(100), + ); + + const profitDiff = estimatedProfit.minus(otherPoolsEstimation.estimatedProfit); + + if (profitDiff.isGreaterThan(0)) { + return t('staking.estimated_profit_compare', { + amount: formatter.format(profitDiff), + }); + } + } + const balance = new BigNumber(tonBalance); if (balance.isGreaterThanOrEqualTo(10)) { @@ -156,7 +193,7 @@ export const Staking: FC = () => { }); } }, - [tonBalance], + [otherPoolsEstimation, tonBalance], ); useEffect(() => { diff --git a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx index 28bdab961..f56cfac60 100644 --- a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx +++ b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx @@ -2,7 +2,6 @@ import { usePoolInfo } from '$hooks/usePoolInfo'; import { useStakingRefreshControl } from '$hooks/useStakingRefreshControl'; import { MainStackRouteNames, openDAppBrowser, openJetton } from '$navigation'; import { MainStackParamList } from '$navigation/MainStack'; -import { NextCycle } from '$shared/components'; import { getServerConfig, KNOWN_STAKING_IMPLEMENTATIONS } from '$shared/constants'; import { getStakingPoolByAddress, getStakingProviderById, useStakingStore } from '$store'; import { @@ -319,20 +318,14 @@ export const StakingPoolDetails: FC = (props) => { ) : null} - {hasAnyBalance ? ( + {/* {hasAnyBalance && stakingJetton && isLiquidTF ? ( <> - - - {/* {stakingJetton && isLiquidTF ? ( - <> - - - - - - ) : null} */} + + + + - ) : null} + ) : null} */} {t('staking.details.about_pool')} diff --git a/packages/mobile/src/core/StakingSend/StakingSend.tsx b/packages/mobile/src/core/StakingSend/StakingSend.tsx index a49f503bb..2ae0cdbc0 100644 --- a/packages/mobile/src/core/StakingSend/StakingSend.tsx +++ b/packages/mobile/src/core/StakingSend/StakingSend.tsx @@ -13,7 +13,7 @@ import { CryptoCurrencies, Decimals } from '$shared/constants'; import { getStakingPoolByAddress, Toast, useStakingStore } from '$store'; import { walletSelector } from '$store/wallet'; import { NavBar } from '$uikit'; -import { calculateActionsTotalAmount, delay, parseLocaleNumber } from '$utils'; +import { calculateMessageTransferAmount, delay, parseLocaleNumber } from '$utils'; import { getTimeSec } from '$utils/getTimeSec'; import { RouteProp } from '@react-navigation/native'; import axios from 'axios'; @@ -268,7 +268,7 @@ export const StakingSend: FC = (props) => { try { setSending(true); - const totalAmount = calculateActionsTotalAmount(messages.current); + const totalAmount = calculateMessageTransferAmount(messages.current); const checkResult = await checkIsInsufficient(totalAmount); if (checkResult.insufficient) { const stakingFee = Ton.fromNano(getWithdrawalFee(pool)); @@ -289,7 +289,11 @@ export const StakingSend: FC = (props) => { .minus(new BigNumber(totalAmount)) .isGreaterThanOrEqualTo(getWithdrawalAlertFee(pool)); - if (!isEnoughToWithdraw && isDeposit) { + if ( + pool.implementation !== PoolImplementationType.LiquidTF && + !isEnoughToWithdraw && + isDeposit + ) { const shouldContinue = await new Promise((res) => Alert.alert( t('staking.withdrawal_fee_warning.title'), diff --git a/packages/mobile/src/core/StakingSend/steps/AmountStep/AmountStep.tsx b/packages/mobile/src/core/StakingSend/steps/AmountStep/AmountStep.tsx index 1f6907366..06429804b 100644 --- a/packages/mobile/src/core/StakingSend/steps/AmountStep/AmountStep.tsx +++ b/packages/mobile/src/core/StakingSend/steps/AmountStep/AmountStep.tsx @@ -67,7 +67,19 @@ const AmountStepComponent: FC = (props) => { isLiquidJetton, } = useCurrencyToSend(currency, isJetton); - const walletBalance = isLiquidJetton ? price!.totalTon : tonBalance; + const availableTonBalance = useMemo(() => { + if (pool.implementation === PoolImplementationType.LiquidTF && !isWithdrawal) { + const tonAmount = new BigNumber(tonBalance).minus(1.2); + + return tonAmount.isGreaterThanOrEqualTo(0) + ? tonAmount.decimalPlaces(Decimals[CryptoCurrencies.Ton]).toString() + : '0'; + } + + return tonBalance; + }, [isWithdrawal, pool.implementation, tonBalance]); + + const walletBalance = isLiquidJetton ? price!.totalTon : availableTonBalance; const minAmount = isWithdrawal ? '0' : Ton.fromNano(pool.min_stake); diff --git a/packages/mobile/src/database/wallet.ts b/packages/mobile/src/database/wallet.ts index 6768914ad..22610a374 100644 --- a/packages/mobile/src/database/wallet.ts +++ b/packages/mobile/src/database/wallet.ts @@ -70,11 +70,55 @@ export async function getBalances(): Promise<{ [index: string]: string }> { } } +export async function getBalancesUpdatedAt(): Promise { + const wallet = getWalletName(); + const raw = await AsyncStorage.getItem(`${wallet}_balances_updated_at`); + + if (!raw) { + return null; + } + + try { + return JSON.parse(raw); + } catch (e) { + return null; + } +} + +export async function saveBalancesUpdatedAtToDB(updatedAt: number) { + const wallet = getWalletName(); + await AsyncStorage.setItem(`${wallet}_balances_updated_at`, JSON.stringify(updatedAt)); +} + export async function saveBalancesToDB(balances: { [index: string]: string }) { const wallet = getWalletName(); await AsyncStorage.setItem(`${wallet}_balances`, JSON.stringify(balances)); } +export async function getOldWalletBalances(): Promise< + { balance: string; version: string }[] +> { + const wallet = getWalletName(); + const raw = await AsyncStorage.getItem(`${wallet}_old_balances`); + + if (!raw) { + return []; + } + + try { + return JSON.parse(raw); + } catch (e) { + return []; + } +} + +export async function saveOldBalancesToDB( + balances: { balance: string; version: string }[], +) { + const wallet = getWalletName(); + await AsyncStorage.setItem(`${wallet}_old_balances`, JSON.stringify(balances)); +} + export async function setMigrationState(state: MigrationState | null) { if (!state) { await AsyncStorage.removeItem('migration_state'); diff --git a/packages/mobile/src/hooks/useBalanceUpdater.ts b/packages/mobile/src/hooks/useBalanceUpdater.ts index 358bf0714..8b1fd11da 100644 --- a/packages/mobile/src/hooks/useBalanceUpdater.ts +++ b/packages/mobile/src/hooks/useBalanceUpdater.ts @@ -1,8 +1,9 @@ import { walletActions, walletWalletSelector } from '$store/wallet'; +import { useNetInfo } from '@react-native-community/netinfo'; import { State } from '@tonkeeper/core'; import { useExternalState } from '@tonkeeper/shared/hooks/useExternalState'; import { tk } from '@tonkeeper/shared/tonkeeper'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; // temporary fix @@ -11,6 +12,9 @@ export const useBalanceUpdater = () => { const wallet = useSelector(walletWalletSelector); const dispatch = useDispatch(); + const { isConnected } = useNetInfo(); + const prevIsConnected = useRef(isConnected); + const isActivityReloading = useExternalState( tk.wallet?.activityList.state ?? new State({ @@ -28,8 +32,8 @@ export const useBalanceUpdater = () => { return; } - if (isActivityReloading) { + if (isActivityReloading || (isConnected && prevIsConnected.current === false)) { dispatch(walletActions.refreshBalancesPage(false)); } - }, [dispatch, isActivityReloading, wallet]); + }, [dispatch, isActivityReloading, isConnected, wallet]); }; diff --git a/packages/mobile/src/hooks/useExchangeMethodInfo.ts b/packages/mobile/src/hooks/useExchangeMethodInfo.ts index 2f02a55fc..9dbd980cf 100644 --- a/packages/mobile/src/hooks/useExchangeMethodInfo.ts +++ b/packages/mobile/src/hooks/useExchangeMethodInfo.ts @@ -3,17 +3,11 @@ import { IMethod } from '$store/zustand/methodsToBuy/types'; import { useMemo } from 'react'; export function useExchangeMethodInfo(id: string = ''): IMethod | null { - const categories = useMethodsToBuyStore((state) => state.categories); + const allMethods = useMethodsToBuyStore((state) => state.allMethods); const method = useMemo(() => { - for (const category of categories) { - const method = category.items.find((item) => item.id === id); - if (method) { - return method; - } - } - return null; - }, [categories, id]); + return allMethods.find((item) => item.id === id) ?? null; + }, [allMethods, id]); return method; } diff --git a/packages/mobile/src/hooks/usePoolInfo.ts b/packages/mobile/src/hooks/usePoolInfo.ts index 164bd204c..7cef66957 100644 --- a/packages/mobile/src/hooks/usePoolInfo.ts +++ b/packages/mobile/src/hooks/usePoolInfo.ts @@ -21,6 +21,9 @@ import { PoolImplementationType, } from '@tonkeeper/core/src/TonAPI'; import { useGetTokenPrice, useTokenPrice } from './useTokenPrice'; +import { openInsufficientFundsModal } from '$core/ModalContainer/InsufficientFunds/InsufficientFunds'; +import { useCurrencyToSend } from './useCurrencyToSend'; +import { Ton } from '$libs/Ton'; export interface PoolDetailsItem { label: string; @@ -34,6 +37,8 @@ export const usePoolInfo = (pool: PoolInfo, poolStakingInfo?: AccountStakingInfo const wallet = useWallet(); + const { balance: tonBalance } = useCurrencyToSend(CryptoCurrencies.Ton); + const jettonBalances = useSelector(jettonsBalancesSelector); const highestApyPool = useStakingStore((s) => s.highestApyPool, shallow); @@ -113,6 +118,15 @@ export const usePoolInfo = (pool: PoolInfo, poolStakingInfo?: AccountStakingInfo const handleTopUpPress = useCallback(() => { if (wallet) { + const canDeposit = new BigNumber(tonBalance).isGreaterThanOrEqualTo(2.2); + if (pool.implementation === PoolImplementationType.LiquidTF && !canDeposit) { + return openInsufficientFundsModal({ + totalAmount: Ton.toNano(2.2), + balance: Ton.toNano(tonBalance), + isStakingDeposit: true, + }); + } + nav.push(AppStackRouteNames.StakingSend, { poolAddress: pool.address, transactionType: StakingTransactionType.DEPOSIT, @@ -120,7 +134,7 @@ export const usePoolInfo = (pool: PoolInfo, poolStakingInfo?: AccountStakingInfo } else { openRequireWalletModal(); } - }, [nav, pool.address, wallet]); + }, [nav, pool.address, pool.implementation, tonBalance, wallet]); const handleWithdrawalPress = useCallback(() => { if (!hasDeposit) { diff --git a/packages/mobile/src/hooks/useStaking.ts b/packages/mobile/src/hooks/useStaking.ts index 8406bc0e2..604a896a3 100644 --- a/packages/mobile/src/hooks/useStaking.ts +++ b/packages/mobile/src/hooks/useStaking.ts @@ -1,5 +1,6 @@ import { useStakingStore } from '$store'; import { walletSelector, walletWalletSelector } from '$store/wallet'; +import { useNetInfo } from '@react-native-community/netinfo'; import { useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; @@ -10,6 +11,9 @@ export const useStaking = () => { const prevAddress = useRef(address); + const { isConnected } = useNetInfo(); + const prevIsConnected = useRef(isConnected); + const hasWallet = !!wallet; useEffect(() => { @@ -31,11 +35,14 @@ export const useStaking = () => { }, [hasWallet]); useEffect(() => { - if (address !== prevAddress.current) { - useStakingStore.getState().actions.reset(); + if ( + address !== prevAddress.current || + (isConnected && prevIsConnected.current === false) + ) { useStakingStore.getState().actions.fetchPools(); } prevAddress.current = address; - }, [address]); + prevIsConnected.current = isConnected; + }, [address, isConnected]); }; diff --git a/packages/mobile/src/modals/ExchangeModal.tsx b/packages/mobile/src/modals/ExchangeModal.tsx index e1146b0f1..956330dbb 100644 --- a/packages/mobile/src/modals/ExchangeModal.tsx +++ b/packages/mobile/src/modals/ExchangeModal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { Button, Loader, Spacer, View } from '$uikit'; import * as S from '../core/Exchange/Exchange.style'; @@ -6,16 +6,17 @@ import { ExchangeItem } from '../core/Exchange/ExchangeItem/ExchangeItem'; import { t } from '@tonkeeper/shared/i18n'; import { getServerConfigSafe } from '$shared/constants'; import { LayoutAnimation } from 'react-native'; -import { Modal } from '@tonkeeper/uikit'; +import { Modal, SegmentedControl, Text } from '@tonkeeper/uikit'; import { Steezy } from '$styles'; import { useMethodsToBuyStore } from '$store/zustand/methodsToBuy/useMethodsToBuyStore'; import { CategoryType } from '$store/zustand/methodsToBuy/types'; import { openChooseCountry } from '$navigation'; import { useSelectedCountry } from '$store/zustand/methodsToBuy/useSelectedCountry'; +import { CountryButton } from '@tonkeeper/shared/components'; export const ExchangeModal = () => { const [showAll, setShowAll] = React.useState(false); - const { categories, defaultLayout, layoutByCountry } = useMethodsToBuyStore( + const { defaultLayout, layoutByCountry, buy, sell } = useMethodsToBuyStore( (state) => state, ); @@ -25,91 +26,107 @@ export const ExchangeModal = () => { const allRegions = selectedCountry === '*'; - const filteredCategories = useMemo(() => { + const [filteredBuy, filteredSell] = useMemo(() => { const usedLayout = layoutByCountry.find((layout) => layout.countryCode === selectedCountry) || defaultLayout; - return categories.map((category) => { - if (category.type !== CategoryType.BUY) { - return category; - } + return [buy, sell].map((tab) => + tab.map((category) => { + if (category.type !== CategoryType.BUY) { + return category; + } - const items = - showAll || allRegions - ? category.items - : category.items.filter((item) => usedLayout.methods.includes(item.id)); + const items = + showAll || allRegions + ? category.items + : category.items.filter((item) => usedLayout.methods.includes(item.id)); - return { - ...category, - items: items.sort((a, b) => { - const aIdx = usedLayout.methods.indexOf(a.id); - const bIdx = usedLayout.methods.indexOf(b.id); - if (aIdx === -1) { - return 1; - } - if (bIdx === -1) { - return -1; - } - return aIdx - bIdx; - }), - }; - }); - }, [layoutByCountry, defaultLayout, categories, selectedCountry, showAll, allRegions]); + return { + ...category, + items: items.sort((a, b) => { + const aIdx = usedLayout.methods.indexOf(a.id); + const bIdx = usedLayout.methods.indexOf(b.id); + if (aIdx === -1) { + return 1; + } + if (bIdx === -1) { + return -1; + } + return aIdx - bIdx; + }), + }; + }), + ); + }, [layoutByCountry, defaultLayout, buy, sell, selectedCountry, showAll, allRegions]); const handleShowAll = useCallback(() => { setShowAll(!showAll); LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); }, [showAll]); + const [segmentIndex, setSegmentIndex] = useState(0); + + const isLoading = buy.length === 0 && sell.length === 0; + + const categories = segmentIndex === 0 ? filteredBuy : filteredSell; + return ( + } + title={ + setSegmentIndex(segment)} + index={segmentIndex} + items={[t('exchange_modal.buy'), t('exchange_modal.sell')]} + /> + } /> - - - - {!categories.length ? ( - - - - ) : ( - <> - {filteredCategories.map((category, cIndex) => ( - - {cIndex > 0 ? : null} - - {category.items.map((item, idx, arr) => ( - - ))} - - {otherWaysAvailable && category.type === 'buy' && !allRegions ? ( - - - - ) : null} - - ))} - - )} - - + + + {isLoading ? ( + + + + ) : ( + <> + {categories.map((category, cIndex) => ( + + {cIndex > 0 ? : null} + + {category.title} + + + {category.items.map((item, idx, arr) => ( + + ))} + + {otherWaysAvailable && category.type === 'buy' && !allRegions ? ( + + + + ) : null} + + ))} + + )} + ); diff --git a/packages/mobile/src/navigation/AppStack/AppStack.interface.ts b/packages/mobile/src/navigation/AppStack/AppStack.interface.ts index e99594b0b..d3468e6b7 100644 --- a/packages/mobile/src/navigation/AppStack/AppStack.interface.ts +++ b/packages/mobile/src/navigation/AppStack/AppStack.interface.ts @@ -24,6 +24,7 @@ export type AppStackParamList = { withGoBack?: boolean; from?: SendAnalyticsFrom; expiryTimestamp?: number | null; + redirectToActivity?: boolean; }; [AppStackRouteNames.ScanQR]: { onScan: (url: string) => boolean | Promise; diff --git a/packages/mobile/src/navigation/ModalStack.tsx b/packages/mobile/src/navigation/ModalStack.tsx index 6226f8e04..55b83f58b 100644 --- a/packages/mobile/src/navigation/ModalStack.tsx +++ b/packages/mobile/src/navigation/ModalStack.tsx @@ -2,13 +2,6 @@ import React from 'react'; import { SecurityMigrationStack } from './SecurityMigrationStack/SecurityMigrationStack'; import { ResetPinStack } from './ResetPinStack/ResetPinStack'; import { createModalStackNavigator } from '@tonkeeper/router'; -import { NFTSingleDeployModal } from '$core/ModalContainer/NFTOperations/Modals/NFTSingleDeployModal'; -import { NFTTransferModal } from '$core/ModalContainer/NFTOperations/Modals/NFTTransferModal'; -import { NFTSaleCancelModal } from '$core/ModalContainer/NFTOperations/Modals/NFTSaleCancelModal'; -import { NFTSalePlaceGetgemsModal } from '$core/ModalContainer/NFTOperations/Modals/NFTSalePlaceGetgemsModal'; -import { NFTSalePlaceModal } from '$core/ModalContainer/NFTOperations/Modals/NFTSalePlaceModal'; -import { NFTItemDeployModal } from '$core/ModalContainer/NFTOperations/Modals/NFTItemDeployModal'; -import { NFTCollectionDeployModal } from '$core/ModalContainer/NFTOperations/Modals/NFTCollectionDeployModal'; import { NFTTransferInputAddressModal } from '$core/ModalContainer/NFTTransferInputAddressModal/NFTTransferInputAddressModal'; import { NFT } from '$core/NFT/NFT'; import { SignRawModal } from '$core/ModalContainer/NFTOperations/Modals/SignRawModal'; @@ -47,16 +40,9 @@ const Stack = createModalStackNavigator(ProvidersWithNavigation); export const ModalStack = React.memo(() => ( - - - - - - - - + { return store.getState().wallet.wallet; @@ -70,6 +76,7 @@ export function useDeeplinkingResolvers() { 'tonkeeper://', 'https://app.tonkeeper.com', 'https://tonhub.com', + 'https://ton.app', ]); deeplinking.addMiddleware(async (next) => { @@ -305,6 +312,7 @@ export function useDeeplinkingResolvers() { isInactive: details.isInactive, isJetton: true, expiryTimestamp, + redirectToActivity: resolveParams.redirectToActivity, }; openSend(options); @@ -330,6 +338,7 @@ export function useDeeplinkingResolvers() { isInactive: details.isInactive, methodId: resolveParams.methodId, expiryTimestamp, + redirectToActivity: resolveParams.redirectToActivity, }; if (options.methodId) { nav.openModal('NewConfirmSending', options); @@ -351,14 +360,39 @@ export function useDeeplinkingResolvers() { withGoBack: resolveParams.withGoBack, isJetton: true, expiryTimestamp, + redirectToActivity: resolveParams.redirectToActivity, }); } else if (query.nft) { if (!Address.isValid(query.nft)) { return Toast.fail(t('transfer_deeplink_nft_address_error')); } - await checkFundsAndOpenNFTTransfer(query.nft, address); + await openSignRawModal( + { + messages: [ + { + amount: AmountFormatter.toNano(1), + address: query.nft, + payload: ContractService.createNftTransferBody({ + queryId: Date.now(), + newOwnerAddress: address, + excessesAddress: tk.wallet.address.ton.raw, + }) + .toBoc() + .toString('base64'), + }, + ], + }, + {}, + ); } else { - openSend({ currency, address, comment, isJetton: false, expiryTimestamp }); + openSend({ + currency, + address, + comment, + isJetton: false, + expiryTimestamp, + redirectToActivity: resolveParams.redirectToActivity, + }); } }); diff --git a/packages/mobile/src/shared/components/NextCycle/NextCycle.style.ts b/packages/mobile/src/shared/components/NextCycle/NextCycle.style.ts index cb0c258d9..a9a2d026d 100644 --- a/packages/mobile/src/shared/components/NextCycle/NextCycle.style.ts +++ b/packages/mobile/src/shared/components/NextCycle/NextCycle.style.ts @@ -1,21 +1,10 @@ import styled from '$styled'; -import { changeAlphaValue, convertHexToRGBA, ns } from '$utils'; -import Animated from 'react-native-reanimated'; +import { ns } from '$utils'; export const Container = styled.View` background: ${({ theme }) => theme.colors.backgroundSecondary}; border-radius: ${ns(16)}px; padding: ${ns(16)}px; - overflow: hidden; - position: relative; -`; - -export const ProgressView = styled(Animated.View)` - position: absolute; - top: 0; - left: 0; - bottom: 0; - background: ${({ theme }) => theme.colors.backgroundTertiary}; `; export const Row = styled.View` diff --git a/packages/mobile/src/shared/components/NextCycle/NextCycle.tsx b/packages/mobile/src/shared/components/NextCycle/NextCycle.tsx index 96a6214a3..3395748dd 100644 --- a/packages/mobile/src/shared/components/NextCycle/NextCycle.tsx +++ b/packages/mobile/src/shared/components/NextCycle/NextCycle.tsx @@ -1,40 +1,20 @@ import { useStakingCycle } from '$hooks/useStakingCycle'; import { Text } from '$uikit'; -import React, { FC, memo, useCallback } from 'react'; +import React, { FC, memo } from 'react'; import * as S from './NextCycle.style'; -import { LayoutChangeEvent } from 'react-native'; -import { interpolate, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; import { t } from '@tonkeeper/shared/i18n'; import { PoolInfo, PoolImplementationType } from '@tonkeeper/core/src/TonAPI'; interface Props { pool: PoolInfo; - nextReward?: string; } const NextCycleComponent: FC = (props) => { const { pool: { cycle_start, cycle_end, implementation }, - nextReward, } = props; - const { formattedDuration, progress, isCooldown } = useStakingCycle( - cycle_start, - cycle_end, - ); - - const containerWidth = useSharedValue(0); - - const handleLayout = useCallback( - (event: LayoutChangeEvent) => { - containerWidth.value = event.nativeEvent.layout.width; - }, - [containerWidth], - ); - - const progressAnimatedStyle = useAnimatedStyle(() => ({ - width: interpolate(progress.value, [0, 1], [0, containerWidth.value]), - })); + const { formattedDuration, isCooldown } = useStakingCycle(cycle_start, cycle_end); if (isCooldown && implementation !== PoolImplementationType.LiquidTF) { return ( @@ -51,33 +31,10 @@ const NextCycleComponent: FC = (props) => { } return ( - - - - - {nextReward ? `+ ${nextReward} TON` : t('staking.details.next_cycle.title')} - - - {t('staking.details.next_cycle.in')}{' '} - - {formattedDuration} - - - - {!nextReward ? ( - - {implementation === PoolImplementationType.LiquidTF - ? t('staking.details.next_cycle.desc_liquid') - : t('staking.details.next_cycle.desc')} - - ) : null} + + + {t('staking.details.next_cycle.message', { value: formattedDuration })} + ); }; diff --git a/packages/mobile/src/store/jettons/manager/providers/ton.ts b/packages/mobile/src/store/jettons/manager/providers/ton.ts index 298b98d68..5ffd92617 100644 --- a/packages/mobile/src/store/jettons/manager/providers/ton.ts +++ b/packages/mobile/src/store/jettons/manager/providers/ton.ts @@ -6,7 +6,12 @@ import { JettonBalanceModel, JettonMetadata } from '$store/models'; import _ from 'lodash'; import { fromNano } from '$utils'; import { proxyMedia } from '$utils/proxyMedia'; -import { JettonsApi, Configuration, AccountsApi, JettonBalance } from '@tonkeeper/core/src/legacy'; +import { + JettonsApi, + Configuration, + AccountsApi, + JettonBalance, +} from '@tonkeeper/core/src/legacy'; export class TonProvider extends BaseProvider { public readonly name = 'TonProvider'; @@ -36,33 +41,28 @@ export class TonProvider extends BaseProvider { } async load(): Promise { - try { - const endpoint = getServerConfig('tonapiV2Endpoint'); - - const accountsApi = new AccountsApi( - new Configuration({ - basePath: endpoint, - headers: { - Authorization: `Bearer ${getServerConfig('tonApiV2Key')}`, - }, - }), - ); + const endpoint = getServerConfig('tonapiV2Endpoint'); - const jettonBalances = await accountsApi.getJettonsBalances({ - accountId: this.address, - }); + const accountsApi = new AccountsApi( + new Configuration({ + basePath: endpoint, + headers: { + Authorization: `Bearer ${getServerConfig('tonApiV2Key')}`, + }, + }), + ); - const balances = jettonBalances.balances; + const jettonBalances = await accountsApi.getJettonsBalances({ + accountId: this.address, + }); - if (!_.isArray(balances)) { - return []; - } + const balances = jettonBalances.balances; - return balances.map((balance) => this.map(balance)); - } catch (e) { - console.log(e); + if (!_.isArray(balances)) { return []; } + + return balances.map((balance) => this.map(balance)); } private map(jettonBalance: JettonBalance): JettonBalanceModel { diff --git a/packages/mobile/src/store/main/sagas.ts b/packages/mobile/src/store/main/sagas.ts index d8ddd8058..4c0a74244 100644 --- a/packages/mobile/src/store/main/sagas.ts +++ b/packages/mobile/src/store/main/sagas.ts @@ -31,10 +31,12 @@ import { import { getAddedCurrencies, getBalances, + getBalancesUpdatedAt, getHiddenNotifications, getIntroShown, getIsTestnet, getMigrationState, + getOldWalletBalances, getPrimaryFiatCurrency, getSavedLogs, getSavedServerConfig, @@ -133,6 +135,8 @@ export function* initHandler(isTestnet: boolean, canRetry = false) { const isIntroShown = yield call(getIntroShown); const primaryCurrency = yield call(getPrimaryFiatCurrency); const balances = yield call(getBalances); + const oldWalletBalances = yield call(getOldWalletBalances); + const balancesUpdatedAt = yield call(getBalancesUpdatedAt); const isNewSecurityFlow = yield call(MainDB.isNewSecurityFlow); const excludedJettons = yield call(MainDB.getExcludedJettons); const accent = yield call(MainDB.getAccent); @@ -158,6 +162,8 @@ export function* initHandler(isTestnet: boolean, canRetry = false) { walletActions.setCurrencies(currencies), subscriptionsActions.reset(), walletActions.setBalances(balances), + walletActions.setOldWalletBalance(oldWalletBalances), + walletActions.setUpdatedAt(balancesUpdatedAt), jettonsActions.setJettonBalances({ jettonBalances }), ), ); @@ -192,6 +198,8 @@ export function* initHandler(isTestnet: boolean, canRetry = false) { walletActions.setCurrencies(currencies), subscriptionsActions.reset(), walletActions.setBalances(balances), + walletActions.setOldWalletBalance(oldWalletBalances), + walletActions.setUpdatedAt(balancesUpdatedAt), mainActions.setAccent(accent), mainActions.setTonCustomIcon(tonCustomIcon), jettonsActions.setJettonBalances({ jettonBalances }), diff --git a/packages/mobile/src/store/models.ts b/packages/mobile/src/store/models.ts index 393a7e61b..5747245f3 100644 --- a/packages/mobile/src/store/models.ts +++ b/packages/mobile/src/store/models.ts @@ -301,4 +301,5 @@ export enum SendAnalyticsFrom { WalletScreen = 'WalletScreen', TonScreen = 'TonScreen', TokenScreen = 'TokenScreen', + SignRaw = 'SignRaw', } diff --git a/packages/mobile/src/store/nfts/manager/providers/ton.ts b/packages/mobile/src/store/nfts/manager/providers/ton.ts index ff6a77628..04a29ea80 100644 --- a/packages/mobile/src/store/nfts/manager/providers/ton.ts +++ b/packages/mobile/src/store/nfts/manager/providers/ton.ts @@ -63,59 +63,48 @@ export class TonProvider extends BaseProvider { } async fetchByOwnerAddress(): Promise { - try { - if (!this.canMore) { - return []; - } + if (!this.canMore) { + return []; + } - const resp = await this.accountsApi.getNftItemsByOwner({ - accountId: this.address, - limit: 1000, - indirectOwnership: true, - offset: 0, - }); + const resp = await this.accountsApi.getNftItemsByOwner({ + accountId: this.address, + limit: 1000, + indirectOwnership: true, + offset: 0, + }); - const nfts = resp.nftItems; + const nfts = resp.nftItems; - if (!nfts) { - return []; - } - - return nfts; - } catch (e) { - debugLog(e); + if (!nfts) { return []; } + + return nfts; } async loadNext(): Promise { - try { - if (!this.canMore) { - return []; - } + if (!this.canMore) { + return []; + } - const ownedNfts = await this.fetchByOwnerAddress(); - let nfts: NFTModel[] = []; - - const collections = await this.loadCollections( - _.uniq( - ownedNfts - .filter((nft) => nft.collection?.address) - .map((nft) => nft.collection?.address), - ), - ); - - if (ownedNfts?.length) { - nfts = ownedNfts.map((nft) => - this.map(nft, collections[nft.collection?.address]), - ); - } - this.canMore = false; + const ownedNfts = await this.fetchByOwnerAddress(); + let nfts: NFTModel[] = []; - return nfts; - } catch (e) { - return []; + const collections = await this.loadCollections( + _.uniq( + ownedNfts + .filter((nft) => nft.collection?.address) + .map((nft) => nft.collection?.address), + ), + ); + + if (ownedNfts?.length) { + nfts = ownedNfts.map((nft) => this.map(nft, collections[nft.collection?.address])); } + this.canMore = false; + + return nfts; } async loadNFTItem(address: string): Promise { @@ -124,7 +113,9 @@ export class TonProvider extends BaseProvider { }); let nft = nfts.nftItems[0]; - if (!nft) throw new Error('NFT item not loaded'); + if (!nft) { + throw new Error('NFT item not loaded'); + } let collection: CollectionModel | undefined; if (nft.collection) { diff --git a/packages/mobile/src/store/wallet/index.ts b/packages/mobile/src/store/wallet/index.ts index 265aaa128..7f0392ef4 100644 --- a/packages/mobile/src/store/wallet/index.ts +++ b/packages/mobile/src/store/wallet/index.ts @@ -25,6 +25,7 @@ import { WalletGetUnlockedVaultAction, RefreshBalancesPageAction, SetReadableAddress, + SetUpdatedAtAction, } from '$store/wallet/interface'; import { SwitchVersionAction } from '$store/main/interface'; import { SelectableVersions } from '$shared/constants'; @@ -40,6 +41,7 @@ const initialState: WalletState = { address: {}, oldWalletBalances: [], readableAddress: null, + updatedAt: null, }; export const { actions, reducer } = createSlice({ @@ -74,6 +76,9 @@ export const { actions, reducer } = createSlice({ }; state.isRefreshing = false; }, + setUpdatedAt(state, action: SetUpdatedAtAction) { + state.updatedAt = action.payload; + }, endLoading(state) { state.isLoaded = true; }, @@ -172,3 +177,8 @@ export const isLockupWalletSelector = createSelector( walletSelector, (walletState) => !!walletState.wallet?.ton.isLockup(), ); + +export const walletUpdatedAtSelector = createSelector( + walletSelector, + (walletState) => walletState.updatedAt, +); diff --git a/packages/mobile/src/store/wallet/interface.ts b/packages/mobile/src/store/wallet/interface.ts index 07329ecca..200007e88 100644 --- a/packages/mobile/src/store/wallet/interface.ts +++ b/packages/mobile/src/store/wallet/interface.ts @@ -23,6 +23,7 @@ export interface WalletState { readableAddress: Address | null; currencies: CryptoCurrency[]; balances: { [index: string]: string }; + updatedAt: number | null; address: { [index: string]: string }; oldWalletBalances: OldWalletBalanceItem[]; } @@ -92,6 +93,7 @@ export type WaitMigrationAction = PayloadAction<{ onFail: () => void; }>; export type SetBalancesAction = PayloadAction; +export type SetUpdatedAtAction = PayloadAction; export type DeployWalletAction = PayloadAction<{ onDone: () => void; onFail: () => void; diff --git a/packages/mobile/src/store/wallet/sagas.ts b/packages/mobile/src/store/wallet/sagas.ts index bd7cc94e9..06dcd4ed5 100644 --- a/packages/mobile/src/store/wallet/sagas.ts +++ b/packages/mobile/src/store/wallet/sagas.ts @@ -13,7 +13,12 @@ import BigNumber from 'bignumber.js'; import * as LocalAuthentication from 'expo-local-authentication'; import * as SecureStore from 'expo-secure-store'; -import { walletActions, walletSelector, walletWalletSelector } from '$store/wallet/index'; +import { + walletActions, + walletBalancesSelector, + walletSelector, + walletWalletSelector, +} from '$store/wallet/index'; import { EncryptedVault, jettonTransferAmount, @@ -55,8 +60,10 @@ import { MainDB, saveAddedCurrencies, saveBalancesToDB, + saveBalancesUpdatedAtToDB, saveFavorites, saveHiddenRecentAddresses, + saveOldBalancesToDB, saveSubscriptions, setIntroShown, setIsTestnet, @@ -66,6 +73,7 @@ import { import { batchActions, Toast, + useAddressUpdateStore, useConnectedAppsStore, useNotificationsStore, useStakingStore, @@ -85,7 +93,7 @@ import { Cache as JettonsCache } from '$store/jettons/manager/cache'; import { Tonapi } from '$libs/Tonapi'; import { clearSubscribeStatus } from '$utils/messaging'; import { useRatesStore } from '$store/zustand/rates'; -import { Cell } from 'ton-core'; +import { Cell } from '@ton/core'; import nacl from 'tweetnacl'; import { encryptMessageComment } from '@tonkeeper/core'; import TonWeb from 'tonweb'; @@ -107,6 +115,8 @@ function* generateVaultWorker() { const vault = yield call(Vault.generate, getWalletName()); yield put(walletActions.setGeneratedVault(vault)); yield call(trackEvent, 'generate_wallet'); + // Dismiss addressUpdate banner for new wallets. It's not necessary to show it for newcomers + useAddressUpdateStore.getState().actions.dismiss(); } catch (e) { e && debugLog(e.message); yield call(Alert.alert, 'Wallet generation error', e.message); @@ -163,6 +173,7 @@ function* createWalletWorker(action: CreateWalletAction) { ), ); + yield call(useStakingStore.getState().actions.fetchPools, true); yield put(walletActions.loadBalances()); // yield put(eventsActions.loadEvents({ isReplace: true })); yield put(nftsActions.loadNFTs({ isReplace: true })); @@ -240,8 +251,6 @@ function* loadBalancesWorker() { balances[CryptoCurrencies.Ton] = tonBalance; } - yield call(saveBalances, balances); - yield put( batchActions( walletActions.setBalances(balances), @@ -250,6 +259,8 @@ function* loadBalancesWorker() { ), ); + yield call(saveBalances, balances); + yield fork(checkLegacyBalances); } catch (e) { console.log('loadBalancesTask ERR', e); @@ -282,6 +293,8 @@ function* checkLegacyBalances() { } yield put(walletActions.setOldWalletBalance(balances)); + + yield call(saveOldBalancesToDB, balances); } catch (e) {} } @@ -551,32 +564,41 @@ function* sendCoinsWorker(action: SendCoinsAction) { } function* reloadBalance(currency: CryptoCurrencies) { - const { wallet, balances } = yield select(walletSelector); + try { + const { wallet } = yield select(walletSelector); - const tokenConfig = getTokenConfig(currency); - let amount: string = '0'; + let amount: string = '0'; - if (currency === CryptoCurrencies.Ton && wallet.ton.isLockup()) { - const balances = yield call([wallet.ton, 'getLockupBalances']); + if (currency === CryptoCurrencies.Ton && wallet.ton.isLockup()) { + const balances = yield call([wallet.ton, 'getLockupBalances']); - yield put( - walletActions.setBalances({ + yield put( + walletActions.setBalances({ + [CryptoCurrencies.Ton]: balances[0], + [CryptoCurrencies.TonRestricted]: balances[1], + [CryptoCurrencies.TonLocked]: balances[2], + }), + ); + + yield call(saveBalances, { [CryptoCurrencies.Ton]: balances[0], [CryptoCurrencies.TonRestricted]: balances[1], [CryptoCurrencies.TonLocked]: balances[2], - }), - ); - } else { - if (PrimaryCryptoCurrencies.indexOf(currency) > -1) { - amount = yield call([wallet[currency], 'getBalance']); - } + }); + } else { + if (PrimaryCryptoCurrencies.indexOf(currency) > -1) { + amount = yield call([wallet[currency], 'getBalance']); + } - yield put( - walletActions.setBalances({ + const balances = { [currency]: amount, - }), - ); - } + }; + + yield put(walletActions.setBalances(balances)); + + yield call(saveBalances, balances); + } + } catch {} } function* changeBalanceAndReloadWorker(action: ChangeBalanceAndReloadAction) { @@ -774,6 +796,11 @@ export function* walletGetUnlockedVault(action?: WalletGetUnlockedVaultAction) { function* saveBalances(balances: { [index: string]: string }) { try { yield call(saveBalancesToDB, balances); + + const updatedAt = Date.now(); + + yield put(walletActions.setUpdatedAt(updatedAt)); + yield call(saveBalancesUpdatedAtToDB, updatedAt); } catch (e) {} } diff --git a/packages/mobile/src/store/zustand/methodsToBuy/types.ts b/packages/mobile/src/store/zustand/methodsToBuy/types.ts index f83d1cd7e..b8f9c26fe 100644 --- a/packages/mobile/src/store/zustand/methodsToBuy/types.ts +++ b/packages/mobile/src/store/zustand/methodsToBuy/types.ts @@ -18,6 +18,7 @@ export interface IMethod { title: string; url: string; }[]; + assets?: string[]; } export interface ILayout { @@ -33,14 +34,20 @@ export interface IDefaultLayout { export enum CategoryType { BUY = 'buy', SELL = 'sell', + Swap = 'swap', +} + +export interface IExchangeCategory { + type: CategoryType; + title: string; + items: IMethod[]; } export interface IMethodsToBuyStore { selectedCountry: string; - categories: { - type: CategoryType; - items: IMethod[]; - }[]; + buy: IExchangeCategory[]; + sell: IExchangeCategory[]; + allMethods: IMethod[]; layoutByCountry: ILayout[]; defaultLayout: IDefaultLayout; lastUsedCountries: string[]; diff --git a/packages/mobile/src/store/zustand/methodsToBuy/useMethodsToBuyStore.ts b/packages/mobile/src/store/zustand/methodsToBuy/useMethodsToBuyStore.ts index 7413f1c65..3fb88b361 100644 --- a/packages/mobile/src/store/zustand/methodsToBuy/useMethodsToBuyStore.ts +++ b/packages/mobile/src/store/zustand/methodsToBuy/useMethodsToBuyStore.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { create } from 'zustand'; import { createJSONStorage, persist } from 'zustand/middleware'; -import { IMethodsToBuyStore } from './types'; +import { IExchangeCategory, IMethodsToBuyStore } from './types'; import { getCountry } from 'react-native-localize'; import axios from 'axios'; import { getServerConfig } from '$shared/constants'; @@ -11,11 +11,14 @@ import DeviceInfo from 'react-native-device-info'; import { getIsTestnet } from '$database'; import { fiatCurrencySelector } from '$store/main'; import { store } from '$store'; +import { flatMap, uniqBy } from 'lodash'; const initialState: Omit = { selectedCountry: 'AUTO', layoutByCountry: [], - categories: [], + buy: [], + sell: [], + allMethods: [], defaultLayout: { methods: ['mercuryo', 'neocrypto'], }, @@ -51,7 +54,16 @@ export const useMethodsToBuyStore = create( }, }, ); - set(resp.data.data); + + const allMethods = uniqBy( + flatMap( + [...resp.data.data.buy, ...resp.data.data.sell] as IExchangeCategory[], + (item) => item.items, + ), + 'id', + ); + + set({ ...resp.data.data, allMethods }); }, }, }), @@ -73,17 +85,21 @@ export const useMethodsToBuyStore = create( }, partialize: ({ selectedCountry, - categories, defaultLayout, layoutByCountry, lastUsedCountries, + buy, + sell, + allMethods, }) => ({ selectedCountry, - categories, defaultLayout, layoutByCountry, lastUsedCountries, + buy, + sell, + allMethods, } as IMethodsToBuyStore), }, ), diff --git a/packages/mobile/src/store/zustand/staking/useStakingStore.ts b/packages/mobile/src/store/zustand/staking/useStakingStore.ts index b74db4605..183bb5b86 100644 --- a/packages/mobile/src/store/zustand/staking/useStakingStore.ts +++ b/packages/mobile/src/store/zustand/staking/useStakingStore.ts @@ -74,7 +74,7 @@ export const useStakingStore = create( const [poolsResponse, nominatorsResponse] = await Promise.allSettled([ tonapi.staking.getStakingPools({ available_for: rawAddress, - include_unverified: true, + include_unverified: false, }), tonapi.staking.getAccountNominatorsPools(rawAddress!), ]); @@ -323,8 +323,9 @@ export const useStakingStore = create( set({ chart }); }, - reset: () => - set({ stakingInfo: {}, stakingBalance: '0', status: StakingApiStatus.Idle }), + reset: () => { + set({ stakingInfo: {}, stakingBalance: '0', status: StakingApiStatus.Idle }); + }, increaseMainFlashShownCount: () => { set({ mainFlashShownCount: getState().mainFlashShownCount + 1 }); }, @@ -334,30 +335,10 @@ export const useStakingStore = create( }, }), { - name: 'staking_v3', + name: 'staking_v4', storage: createJSONStorage(() => AsyncStorage), - partialize: ({ - pools, - providers, - stakingInfo, - stakingJettons, - stakingJettonsUpdatedAt, - highestApyPool, - stakingBalance, - mainFlashShownCount, - stakingFlashShownCount, - }) => - ({ - pools, - providers, - stakingInfo, - stakingJettons, - stakingJettonsUpdatedAt, - highestApyPool, - stakingBalance, - mainFlashShownCount, - stakingFlashShownCount, - } as IStakingStore), + partialize: ({ status: _status, actions: _actions, ...state }) => + state as IStakingStore, }, ), ); diff --git a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx index a5b5bf43b..7e5c44db8 100644 --- a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx +++ b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx @@ -17,7 +17,7 @@ import { RefreshControl, useWindowDimensions } from 'react-native'; import { NFTCardItem } from './NFTCardItem'; import { useDispatch, useSelector } from 'react-redux'; import { ns } from '$utils'; -import { walletActions, walletSelector } from '$store/wallet'; +import { walletActions, walletSelector, walletUpdatedAtSelector } from '$store/wallet'; import { copyText } from '$hooks/useCopyText'; import { useIsFocused } from '@react-navigation/native'; import { useBalance } from './hooks/useBalance'; @@ -47,6 +47,9 @@ import { trackEvent } from '$utils/stats'; import { useTronBalances } from '@tonkeeper/shared/query/hooks/useTronBalances'; import { tk } from '@tonkeeper/shared/tonkeeper'; import { ExpiringDomainCell } from './components/ExpiringDomainCell'; +import { useNetInfo } from '@react-native-community/netinfo'; +import { format } from 'date-fns'; +import { getLocale } from '$utils/date'; export const WalletScreen = memo(() => { const flags = useFlags(['disable_swap']); @@ -69,6 +72,10 @@ export const WalletScreen = memo(() => { const notifications = useInternalNotifications(); + const { isConnected } = useNetInfo(); + + const walletUpdatedAt = useSelector(walletUpdatedAtSelector); + // TODO: rewrite useEffect(() => { const timer = setTimeout(() => { @@ -133,7 +140,7 @@ export const WalletScreen = memo(() => { {shouldUpdate && } - {wallet && tk.wallet && ( + {wallet && tk.wallet && isConnected !== false ? ( { {tk.wallet.address.ton.short} - )} + ) : null} + {wallet && tk.wallet && isConnected === false && walletUpdatedAt ? ( + + + {t('wallet.updated_at', { + value: format(walletUpdatedAt, 'd MMM, HH:mm', { + locale: getLocale(), + }).replace('.', ''), + })} + + + ) : null} = (props) => { } if (hasPendingWithdraw) { + if (pool.implementation === PoolImplementationType.LiquidTF) { + return t('staking.message.pendingWithdrawLiquid', { + amount: stakingFormatter.format(pendingWithdraw.amount), + count: pendingWithdraw.totalTon, + }); + } + return ( <> {t('staking.message.pendingWithdraw', { diff --git a/packages/mobile/src/utils/signRawCalculateAmount.ts b/packages/mobile/src/utils/signRawCalculateAmount.ts index 152b6d265..d86c8b7a2 100644 --- a/packages/mobile/src/utils/signRawCalculateAmount.ts +++ b/packages/mobile/src/utils/signRawCalculateAmount.ts @@ -1,18 +1,6 @@ import BigNumber from 'bignumber.js'; import { SignRawMessage } from '$core/ModalContainer/NFTOperations/TXRequest.types'; -import { ActionType, Address } from '@tonkeeper/core'; -import { Action } from '@tonkeeper/core/src/TonAPI'; -import { ActionTypeEnum } from 'tonapi-sdk-js'; - -export const calculateActionsTotalAmount = (messages: SignRawMessage[]) => { - if (!messages.length) { - return 0; - } - return messages.reduce((acc, message) => { - return new BigNumber(acc).plus(new BigNumber(message.amount)).toString(); - }, '0'); -}; export const calculateMessageTransferAmount = (messages: SignRawMessage[]) => { if (!messages) { diff --git a/packages/router/src/hooks/useNavigation.ts b/packages/router/src/hooks/useNavigation.ts index fdc42ce6d..3114dff8b 100644 --- a/packages/router/src/hooks/useNavigation.ts +++ b/packages/router/src/hooks/useNavigation.ts @@ -5,8 +5,7 @@ import { } from '@react-navigation/native'; import { SheetRoutesContext } from '../context/SheetRoutesContext'; import { SheetActions, useCloseModal } from '../SheetsProvider'; -import { nanoid } from 'nanoid/non-secure'; -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; import { throttle, delay, isAndroid } from '../utils'; import { Keyboard } from 'react-native'; @@ -72,9 +71,13 @@ export const useNavigation = () => { } }; - const push = throttle((path: string, params?: any) => { - nav.dispatch(StackActions.push(path, params)); - }, 1000); + const push = useMemo( + () => + throttle((path: string, params?: any) => { + nav.dispatch(StackActions.push(path, params)); + }, 1000), + [], + ); const globalGoBack = () => { if (nav.canGoBack()) { diff --git a/packages/router/src/useSheetMeasurements.ts b/packages/router/src/useSheetMeasurements.ts index 3d406e42b..9ab29bc15 100644 --- a/packages/router/src/useSheetMeasurements.ts +++ b/packages/router/src/useSheetMeasurements.ts @@ -1,5 +1,7 @@ import { SharedValue, useDerivedValue, useSharedValue } from 'react-native-reanimated'; import { INITIAL_SNAP_POINT } from '@gorhom/bottom-sheet/src/components/bottomSheet/constants'; +import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; +import { useCallback } from 'react'; export type LayoutMeasurementEvent = { nativeEvent: { @@ -13,6 +15,8 @@ export type SheetMeasurements = { measureHeader: (ev: LayoutMeasurementEvent) => void; measureContent: (ev: LayoutMeasurementEvent) => void; measureFooter: (ev: LayoutMeasurementEvent) => void; + scrollHandler: (event: NativeSyntheticEvent) => void; + scrollY: SharedValue; snapPoints: SharedValue<(string | number)[]>; handleHeight: SharedValue; headerHeight: SharedValue; @@ -26,6 +30,12 @@ export const useSheetMeasurements = (): SheetMeasurements => { const contentHeight = useSharedValue(0); const footerHeight = useSharedValue(0); + const scrollY = useSharedValue(0); + + const scrollHandler = useCallback((event: NativeSyntheticEvent) => { + scrollY.value = event.nativeEvent.contentOffset.y; + }, []); + const snapPoints = useDerivedValue(() => { if (contentHeight.value === 0) { return [INITIAL_SNAP_POINT]; @@ -51,6 +61,8 @@ export const useSheetMeasurements = (): SheetMeasurements => { measureHeader, measureContent, measureFooter, + scrollHandler, + scrollY, handleHeight, headerHeight, contentHeight, diff --git a/packages/shared/components/CountryButton/CountryButton.tsx b/packages/shared/components/CountryButton/CountryButton.tsx new file mode 100644 index 000000000..521b091b7 --- /dev/null +++ b/packages/shared/components/CountryButton/CountryButton.tsx @@ -0,0 +1,65 @@ +import { Button, View, isAndroid, ns } from '@tonkeeper/uikit'; +import { ButtonProps } from '@tonkeeper/uikit/src/components/Button'; +import { FC, memo } from 'react'; +import { Text as RNText } from 'react-native'; + +const getSelectedCountryStyle = (selectedCountry: string) => { + if (selectedCountry === '*') { + return { + icon: ( + + 🌍 + + ), + type: 'emoji', + }; + } + if (selectedCountry === 'NOKYC') { + return { + icon: ( + + ☠️ + + ), + type: 'emoji', + }; + } + + return { title: selectedCountry, type: 'text' }; +}; + +interface CountryButtonProps extends ButtonProps { + selectedCountry: string; + onPress: () => void; +} + +const CountryButtonComponent: FC = ({ + selectedCountry, + onPress, + ...restProps +}) => { + const selectedCountryStyle = getSelectedCountryStyle(selectedCountry); + + return ( +