feat: initial brc-20 transfers

This commit is contained in:
kyranjamie
2023-05-29 18:55:53 +02:00
committed by Anastasios
parent 2800623fac
commit bc4e1800f6
25 changed files with 215 additions and 32 deletions

View File

@@ -83,5 +83,10 @@
"minValues": [2500, 3000, 3500],
"minValuesEnabled": true
},
"nftMetadataEnabled": true
"nftMetadataEnabled": true,
"ordinalsbot": {
"integrationEnabled": true,
"mainnetApiUrl": "https://ordinalsbot-api2.herokuapp.com",
"signetApiUrl": "https://signet.ordinalsbot.com"
}
}

View File

@@ -127,6 +127,16 @@
"nftMetadataEnabled": {
"type": "boolean",
"description": "Determines whether or not we display nft metadata in the wallet"
},
"ordinalsbot": {
"type": "object",
"description": "Configuration for the ordinalsbot",
"additionalProperties": false,
"properties": {
"integrationEnabled": { "type": "boolean" },
"mainnetApiUrl": { "type": "string" },
"signetApiUrl": { "type": "string" }
}
}
},
"$defs": {

View File

@@ -171,6 +171,7 @@
"are-passive-events-supported": "1.1.1",
"argon2-browser": "1.18.0",
"assert": "2.0.0",
"axios": "1.4.0",
"base64url": "3.0.1",
"bignumber.js": "9.1.1",
"bitcoin-address-validation": "2.2.1",
@@ -211,6 +212,7 @@
"redux-persist": "6.0.0",
"rxjs": "7.8.1",
"ts-debounce": "4.0.0",
"url-join": "5.0.0",
"use-events": "1.4.2",
"use-latest": "1.2.1",
"valid-url": "1.0.9",

View File

@@ -7,7 +7,7 @@ import { SettingsMenuSelectors } from '@tests/selectors/settings.selectors';
import { Tooltip } from '@app/components/tooltip';
import { Caption } from '@app/components/typography';
import { useConfigBitcoinEnabled } from '@app/query/common/hiro-config/hiro-config.query';
import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query';
import { CaptionDotSeparator } from '../caption-dot-separator';
import { AccountActiveCheckmark } from './account-active-checkmark';

View File

@@ -3,7 +3,7 @@ import { forwardRefWithAs } from '@stacks/ui-core';
import { createMoney } from '@shared/models/money.model';
import { Brc20Token } from '@app/query/bitcoin/ordinals/brc20-tokens.query';
import { Brc20Token } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query';
import { Brc20TokenAssetItemLayout } from './brc20-token-asset-item.layout';

View File

@@ -3,7 +3,7 @@ import { useMemo } from 'react';
import { LoadingSpinner } from '@app/components/loading-spinner';
import { useBitcoinPendingTransactions } from '@app/query/bitcoin/address/transactions-by-address.hooks';
import { useGetBitcoinTransactionsByAddressQuery } from '@app/query/bitcoin/address/transactions-by-address.query';
import { useConfigBitcoinEnabled } from '@app/query/common/hiro-config/hiro-config.query';
import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query';
import { useStacksPendingTransactions } from '@app/query/stacks/mempool/mempool.hooks';
import { useGetAccountTransactionsWithTransfersQuery } from '@app/query/stacks/transactions/transactions-with-transfers.query';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';

View File

@@ -11,7 +11,7 @@ import { BtcIcon } from '@app/components/icons/btc-icon';
import { LoadingSpinner } from '@app/components/loading-spinner';
import { Caption } from '@app/components/typography';
import { Brc20TokensLoader } from '@app/features/balances-list/components/brc-20-tokens-loader';
import { useConfigBitcoinEnabled } from '@app/query/common/hiro-config/hiro-config.query';
import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query';
import { useStacksFungibleTokenAssetBalancesAnchoredWithMetadata } from '@app/query/stacks/balance/stacks-ft-balances.hooks';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';

View File

@@ -1,7 +1,7 @@
import { Stack } from '@stacks/ui';
import { Brc20TokenAssetItem } from '@app/components/crypto-assets/bitcoin/brc20-token-asset/brc20-token-asset-item';
import { Brc20Token } from '@app/query/bitcoin/ordinals/brc20-tokens.query';
import { Brc20Token } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query';
interface BitcoinFungibleTokenAssetListProps {
brc20Tokens?: Brc20Token[];

View File

@@ -1,7 +1,7 @@
import {
Brc20Token,
useBrc20TokensByAddressQuery,
} from '@app/query/bitcoin/ordinals/brc20-tokens.query';
} from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query';
import { useCurrentAccountTaprootAddressIndexZeroPayment } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
interface Brc20TokensLoaderProps {

View File

@@ -6,7 +6,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { RouteUrls } from '@shared/route-urls';
import { useWalletType } from '@app/common/use-wallet-type';
import { useConfigNftMetadataEnabled } from '@app/query/common/hiro-config/hiro-config.query';
import { useConfigNftMetadataEnabled } from '@app/query/common/remote-config/remote-config.query';
import { AddCollectible } from './components/add-collectible';
import { Ordinals } from './components/bitcoin/ordinals';

View File

@@ -11,7 +11,7 @@ import { NetworkModeBadge } from '@app/components/network-mode-badge';
import { CurrentAccountAvatar } from '@app/features/current-account/current-account-avatar';
import { CurrentAccountName } from '@app/features/current-account/current-account-name';
import { CurrentStxAddress } from '@app/features/current-account/current-stx-address';
import { useConfigBitcoinEnabled } from '@app/query/common/hiro-config/hiro-config.query';
import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
interface PopupHeaderLayoutProps {

View File

@@ -2,7 +2,7 @@ import { FiX } from 'react-icons/fi';
import { Box, Flex, Text } from '@stacks/ui';
import { HiroMessage } from '@app/query/common/hiro-config/hiro-config.query';
import { HiroMessage } from '@app/query/common/remote-config/remote-config.query';
interface HiroMessageItemProps extends HiroMessage {
onDismiss(id: string): void;

View File

@@ -1,6 +1,6 @@
import { Flex, FlexProps } from '@stacks/ui';
import { useRemoteHiroMessages } from '@app/query/common/hiro-config/hiro-config.query';
import { useRemoteHiroMessages } from '@app/query/common/remote-config/remote-config.query';
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
import { useDismissMessage } from '@app/store/settings/settings.actions';
import { useDismissedMessageIds } from '@app/store/settings/settings.selectors';

View File

@@ -1,6 +1,6 @@
import { FundPageSelectors } from '@tests-legacy/page-objects/fund.selectors';
import { AvailableRegions } from '@app/query/common/hiro-config/hiro-config.query';
import { AvailableRegions } from '@app/query/common/remote-config/remote-config.query';
import { FastCheckoutBadge } from './fast-checkout-badge';
import { FundAccountTile } from './fund-account-tile';

View File

@@ -10,7 +10,7 @@ import { LoadingSpinner } from '@app/components/loading-spinner';
import {
useActiveFiatProviders,
useHasFiatProviders,
} from '@app/query/common/hiro-config/hiro-config.query';
} from '@app/query/common/remote-config/remote-config.query';
import { FiatProviderItem } from './fiat-provider-item';
import { activeFiatProviderIcons, getProviderUrl } from './fiat-providers.utils';

View File

@@ -12,7 +12,7 @@ import { generateOnRampURL } from '@coinbase/cbpay-js';
import { COINBASE_APP_ID, MOONPAY_API_KEY, TRANSAK_API_KEY } from '@shared/environment';
import { ActiveFiatProvider } from '@app/query/common/hiro-config/hiro-config.query';
import { ActiveFiatProvider } from '@app/query/common/remote-config/remote-config.query';
// Keys are set in wallet-config.json
enum ActiveFiatProviders {

View File

@@ -9,7 +9,7 @@ import { HomePageSelectorsLegacy } from '@tests-legacy/page-objects/home.selecto
import { RouteUrls } from '@shared/route-urls';
import { SecondaryButton } from '@app/components/secondary-button';
import { useHasFiatProviders } from '@app/query/common/hiro-config/hiro-config.query';
import { useHasFiatProviders } from '@app/query/common/remote-config/remote-config.query';
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
import { HomeActionButton } from './tx-button';

View File

@@ -7,7 +7,7 @@ import { RouteUrls } from '@shared/route-urls';
import { PrimaryButton } from '@app/components/primary-button';
import { QrCodeIcon } from '@app/components/qr-code-icon';
import { useConfigBitcoinEnabled } from '@app/query/common/hiro-config/hiro-config.query';
import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query';
import { HomeActionButton } from './tx-button';

View File

@@ -8,7 +8,7 @@ import type {
import { RouteUrls } from '@shared/route-urls';
import { CryptoCurrencyAssetItem } from '@app/components/crypto-assets/crypto-currency-asset/crypto-currency-asset-item';
import { useConfigBitcoinSendEnabled } from '@app/query/common/hiro-config/hiro-config.query';
import { useConfigBitcoinSendEnabled } from '@app/query/common/remote-config/remote-config.query';
import { CryptoCurrencyAssetIcon } from './crypto-currency-asset-icon';
import { FungibleTokenAssetItem } from './fungible-token-asset-item';

View File

@@ -0,0 +1,41 @@
import BigNumber from 'bignumber.js';
import * as yup from 'yup';
interface Brc20TransferInscription {
p: 'brc-20';
op: 'transfer';
tick: string;
amt: string;
}
const brc20TransferInscriptionSchema = yup.object({
p: yup.string().required().equals(['brc-20']),
op: yup.string().required().equals(['transfer']),
tick: yup
.string()
.required()
.test(value => value.length > 0 && value.length <= 4),
amt: yup
.string()
.required()
.test(value => new BigNumber(value).isFinite())
.test(value => new BigNumber(value).isInteger()),
});
function validateBrc20TransferInscription(val: unknown): val is Brc20TransferInscription {
return brc20TransferInscriptionSchema.isValidSync(val);
}
// ts-unused-exports:disable-next-line
export function createBrc20TransferInscription(tick: string, amt: string) {
const transfer: Brc20TransferInscription = {
p: 'brc-20',
op: 'transfer',
tick,
amt,
};
if (!validateBrc20TransferInscription(transfer)) throw new Error('Invalid transfer inscription');
return transfer;
}

View File

@@ -0,0 +1,93 @@
import axios from 'axios';
import urlJoin from 'url-join';
import { useConfigOrdinalsbot } from '@app/query/common/remote-config/remote-config.query';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
interface TextInscriptionSuccessResponse {
status: 'ok';
charge: {
id: string;
address: string;
amount: number;
lightning_invoice: {
expires_at: number;
payreq: string;
};
created_at: number;
};
chainFee: number;
serviceFee: number;
orderType: string;
createdAt: number;
}
interface OrderStatusSuccessResponse {
status: string;
paid: boolean;
underpaid: boolean;
expired: boolean;
tx: {
commit: string;
fees: number;
inscription: string;
reveal: string;
};
sent: string;
}
class OrdinalsbotClient {
constructor(readonly baseUrl: string) {}
async isAvailable() {
return axios.get<{ status: string }>(urlJoin(this.baseUrl, 'status'));
}
async textInscription(text: string, receiveAddress: string) {
return axios.post<TextInscriptionSuccessResponse>(urlJoin(this.baseUrl, 'textorder'), {
receiveAddress,
texts: [text],
});
}
async orderStatus(id: string) {
return axios.get<OrderStatusSuccessResponse>(urlJoin(this.baseUrl, 'order'), {
params: { id },
});
}
}
function useOrdinalsbotApiUrl() {
const currentNetwork = useCurrentNetwork();
const ordinalsbotConfig = useConfigOrdinalsbot();
if (currentNetwork.chain.bitcoin.network === 'mainnet') return ordinalsbotConfig.mainnetApiUrl;
return ordinalsbotConfig.signetApiUrl;
}
// ts-unused-exports:disable-next-line
export function useOrdinalsbotClient() {
const apiUrl = useOrdinalsbotApiUrl();
return new OrdinalsbotClient(apiUrl);
}
// ts-unused-exports:disable-next-line
export function useBrc20FeatureFlag() {
const currentNetwork = useCurrentNetwork();
const ordinalsbotConfig = useConfigOrdinalsbot();
if (!ordinalsbotConfig.integrationEnabled) {
return { enabled: false, reason: 'BRC-20 transfers are temporarily disabled' } as const;
}
const supportedNetwork =
currentNetwork.chain.bitcoin.network === 'mainnet' ||
currentNetwork.chain.bitcoin.network === 'signet';
if (!supportedNetwork) return { enabled: false, reason: 'Unsupported network' } as const;
// TODO: Add api availability check
return { enabled: true } as const;
}

View File

@@ -45,13 +45,18 @@ interface FeeEstimationsConfig {
minValuesEnabled?: boolean;
}
interface HiroConfig {
interface RemoteConfig {
messages: any;
activeFiatProviders?: Record<string, ActiveFiatProvider>;
bitcoinEnabled: boolean;
bitcoinSendEnabled: boolean;
feeEstimationsMinMax?: FeeEstimationsConfig;
nftMetadataEnabled: boolean;
ordinalsbot: {
integrationEnabled: boolean;
mainnetApiUrl: string;
signetApiUrl: string;
};
}
// TODO: BRANCH_NAME is not working here for config changes on PR branches
@@ -61,12 +66,12 @@ const githubWalletConfigRawUrl = `https://raw.githubusercontent.com/${GITHUB_ORG
BRANCH_NAME || defaultBranch
}/config/wallet-config.json`;
async function fetchHiroMessages(): Promise<HiroConfig> {
if (!BRANCH_NAME && WALLET_ENVIRONMENT !== 'production') return localConfig as HiroConfig;
async function fetchHiroMessages(): Promise<RemoteConfig> {
if (!BRANCH_NAME && WALLET_ENVIRONMENT !== 'production') return localConfig as RemoteConfig;
return fetch(githubWalletConfigRawUrl).then(msg => msg.json());
}
function useRemoteHiroConfig() {
function useRemoteConfig() {
const { data } = useQuery(['walletConfig'], fetchHiroMessages, {
// As we're fetching from Github, a third-party, we want
// to avoid any unnecessary stress on their services, so
@@ -78,12 +83,12 @@ function useRemoteHiroConfig() {
}
export function useRemoteHiroMessages(): HiroMessage[] {
const config = useRemoteHiroConfig();
const config = useRemoteConfig();
return get(config, 'messages.global', []);
}
export function useActiveFiatProviders() {
const config = useRemoteHiroConfig();
const config = useRemoteConfig();
if (!config?.activeFiatProviders) return {} as Record<string, ActiveFiatProvider>;
return Object.fromEntries(
@@ -101,7 +106,7 @@ export function useHasFiatProviders() {
export function useConfigBitcoinEnabled() {
const { whenWallet } = useWalletType();
const config = useRemoteHiroConfig();
const config = useRemoteConfig();
return whenWallet({
ledger: false,
software: config?.bitcoinEnabled ?? true,
@@ -110,7 +115,7 @@ export function useConfigBitcoinEnabled() {
export function useConfigBitcoinSendEnabled() {
const { whenWallet } = useWalletType();
const config = useRemoteHiroConfig();
const config = useRemoteConfig();
return whenWallet({
ledger: false,
software: config?.bitcoinSendEnabled ?? true,
@@ -118,13 +123,13 @@ export function useConfigBitcoinSendEnabled() {
}
export function useConfigFeeEstimationsMaxEnabled() {
const config = useRemoteHiroConfig();
const config = useRemoteConfig();
if (isUndefined(config) || isUndefined(config?.feeEstimationsMinMax)) return;
return config.feeEstimationsMinMax.maxValuesEnabled;
}
export function useConfigFeeEstimationsMaxValues() {
const config = useRemoteHiroConfig();
const config = useRemoteConfig();
if (typeof config?.feeEstimationsMinMax === 'undefined') return;
if (!config.feeEstimationsMinMax.maxValues) return;
if (!Array.isArray(config.feeEstimationsMinMax.maxValues)) return;
@@ -132,13 +137,13 @@ export function useConfigFeeEstimationsMaxValues() {
}
export function useConfigFeeEstimationsMinEnabled() {
const config = useRemoteHiroConfig();
const config = useRemoteConfig();
if (isUndefined(config) || isUndefined(config?.feeEstimationsMinMax)) return;
return config.feeEstimationsMinMax.minValuesEnabled;
}
export function useConfigFeeEstimationsMinValues() {
const config = useRemoteHiroConfig();
const config = useRemoteConfig();
if (typeof config?.feeEstimationsMinMax === 'undefined') return;
if (!config.feeEstimationsMinMax.minValues) return;
if (!Array.isArray(config.feeEstimationsMinMax.minValues)) return;
@@ -146,6 +151,19 @@ export function useConfigFeeEstimationsMinValues() {
}
export function useConfigNftMetadataEnabled() {
const config = useRemoteHiroConfig();
const config = useRemoteConfig();
return config?.nftMetadataEnabled ?? true;
}
export function useConfigOrdinalsbot() {
const config = useRemoteConfig();
return {
integrationEnabled: get(config, 'ordinalsbot.integrationEnabled', true),
mainnetApiUrl: get(
config,
'ordinalsbot.mainnetApiUrl',
'https://ordinalsbot-api2.herokuapp.com'
),
signetApiUrl: get(config, 'ordinalsbot.signetApiUrl', 'https://signet.ordinalsbot.com'),
};
}

View File

@@ -12,7 +12,7 @@ import {
useConfigFeeEstimationsMaxValues,
useConfigFeeEstimationsMinEnabled,
useConfigFeeEstimationsMinValues,
} from '@app/query/common/hiro-config/hiro-config.query';
} from '@app/query/common/remote-config/remote-config.query';
import { useGetStacksTransactionFeeEstimationQuery } from '@app/query/stacks/fees/fees.query';
import {

View File

@@ -7109,6 +7109,15 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
axios@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axios@^0.25.0:
version "0.25.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a"
@@ -10425,7 +10434,7 @@ fluent-syntax@0.13.0:
resolved "https://registry.yarnpkg.com/fluent-syntax/-/fluent-syntax-0.13.0.tgz#417144d99cba94ff474c422b3e6623d5a842855a"
integrity sha512-0Bk1AsliuYB550zr4JV9AYhsETsD3ELXUQzdXGJfIc1Ni/ukAfBdQInDhVMYJUaT2QxoamNslwkYF7MlOrPUwg==
follow-redirects@^1.0.0, follow-redirects@^1.14.7:
follow-redirects@^1.0.0, follow-redirects@^1.14.7, follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
@@ -17199,6 +17208,11 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
url-join@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-5.0.0.tgz#c2f1e5cbd95fa91082a93b58a1f42fecb4bdbcf1"
integrity sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==
url-parse@^1.5.3:
version "1.5.10"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"