diff --git a/CHANGELOG.md b/CHANGELOG.md index ad39eebb..2ee193dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- added: `EdgeCurrencyConfig.encodePayLink` +- added: `EdgeCurrencyConfig.parseLink` +- deprecated: `EdgeCurrencyWallet.encodeUri`. Use `EdgeCurrencyConfig.encodePayLink` +- deprecated: `EdgeCurrencyWallet.parseUri`. Use `EdgeCurrencyConfig.parseLink` - fixed: Avoid deprecated Gradle syntax. ## 2.34.0 (2025-08-25) diff --git a/src/core/account/plugin-api.ts b/src/core/account/plugin-api.ts index 7bff1070..0724ec5a 100644 --- a/src/core/account/plugin-api.ts +++ b/src/core/account/plugin-api.ts @@ -5,16 +5,20 @@ import { EdgeCurrencyInfo, EdgeGetTokenDetailsFilter, EdgeOtherMethods, + EdgeParsedLink, + EdgePayLink, EdgeSwapConfig, EdgeSwapInfo, EdgeToken, + EdgeTokenId, EdgeTokenMap } from '../../types/types' +import { parsedUriToLink } from '../currency/uri-tools' import { uniqueStrings } from '../currency/wallet/enabled-tokens' import { getCurrencyTools } from '../plugins/plugins-selectors' import { ApiInput } from '../root-pixie' import { changePluginUserSettings, changeSwapSettings } from './account-files' -import { getTokenId } from './custom-tokens' +import { getTokenId, makeMetaTokens } from './custom-tokens' const emptyTokens: EdgeTokenMap = {} const emptyTokenIds: string[] = [] @@ -186,6 +190,54 @@ export class CurrencyConfig ) } + async parseLink( + link: string, + opts: { tokenId?: EdgeTokenId } = {} + ): Promise { + const { tokenId } = opts + const { allTokens } = this + const tools = await getCurrencyTools(this._ai, this._pluginId) + + if (tools.parseLink != null) { + return await tools.parseLink(link, { tokenId, allTokens }) + } + + // Fallback version: + if (tools.parseUri != null) { + const out = await tools.parseUri( + link, + this.downgradeTokenId(tokenId), + makeMetaTokens(this.customTokens) + ) + return parsedUriToLink(out, this.currencyInfo, allTokens) + } + + return {} + } + + async encodePayLink(link: EdgePayLink): Promise { + const { tokenId } = link + const { allTokens } = this + const tools = await getCurrencyTools(this._ai, this._pluginId) + + if (tools.encodePayLink != null) { + return await tools.encodePayLink(link, { allTokens }) + } + + // Fallback version: + if (tools.encodeUri != null) { + return await tools.encodeUri( + { + ...link, + currencyCode: this.downgradeTokenId(tokenId) + }, + makeMetaTokens(this.customTokens) + ) + } + + return '' + } + async importKey( userInput: string, opts: { keyOptions?: object } = {} @@ -198,6 +250,13 @@ export class CurrencyConfig const keys = await tools.importPrivateKey(userInput, opts.keyOptions) return { ...keys, imported: true } } + + private downgradeTokenId(tokenId?: EdgeTokenId): string | undefined { + if (tokenId === undefined) return + return tokenId == null + ? this.currencyInfo.currencyCode + : this.allTokens[tokenId]?.currencyCode + } } export class SwapConfig extends Bridgeable { diff --git a/src/core/currency/uri-tools.ts b/src/core/currency/uri-tools.ts new file mode 100644 index 00000000..908d2410 --- /dev/null +++ b/src/core/currency/uri-tools.ts @@ -0,0 +1,163 @@ +import { + EdgeCurrencyInfo, + EdgeParsedLink, + EdgeParsedUri, + EdgeTokenMap +} from '../../types/types' +import { makeMetaToken } from '../account/custom-tokens' + +export function parsedUriToLink( + uri: EdgeParsedUri, + currencyInfo: EdgeCurrencyInfo, + allTokens: EdgeTokenMap +): EdgeParsedLink { + const { + // Edge has never supported BitID: + // bitIDCallbackUri, + // bitIDDomain, + // bitidKycProvider, // Experimental + // bitidKycRequest, // Experimental + // bitidPaymentAddress, // Experimental + // bitIDURI, + + // The GUI handles address requests: + // returnUri, + + currencyCode, + expireDate, + legacyAddress, + metadata, + minNativeAmount, + nativeAmount, + paymentProtocolUrl, + privateKeys, + publicAddress, + segwitAddress, + token, + uniqueIdentifier, + walletConnect + } = uri + let { tokenId } = uri + + if (tokenId === undefined && currencyCode != null) { + tokenId = + currencyCode === currencyInfo.currencyCode + ? null + : Object.keys(allTokens).find( + tokenId => allTokens[tokenId].currencyCode === currencyCode + ) + } + + const out: EdgeParsedLink = {} + + // Payment addresses: + if (publicAddress != null) { + out.pay = { + publicAddress: legacyAddress ?? publicAddress ?? segwitAddress, + addressType: + legacyAddress != null + ? 'legacyAddress' + : publicAddress != null + ? 'publicAddress' + : 'segwitAddress', + label: metadata?.name, + message: metadata?.notes, + memo: uniqueIdentifier, + memoType: 'text', + nativeAmount: nativeAmount, + minNativeAmount: minNativeAmount, + tokenId: tokenId, + expires: expireDate, + isGateway: (metadata as any)?.gateway // Undocumented feature + } + } + + if (paymentProtocolUrl != null) { + out.paymentProtocol = { paymentProtocolUrl } + } + + // Private keys: + if (privateKeys != null && privateKeys.length > 0) { + out.privateKey = { privateKey: privateKeys[0] } + } + + // Custom tokens: + if (token != null) { + const { contractAddress, currencyCode, currencyName, denominations } = token + out.token = { + currencyCode, + denominations, + displayName: currencyName, + networkLocation: { + contractAddress, + type: (token as any).type // Undocumented EVM token type + } + } + } + + if (walletConnect != null) { + out.walletConnect = walletConnect + } + + return out +} + +export function linkToParsedUri(link: EdgeParsedLink): EdgeParsedUri { + const out: EdgeParsedUri = {} + + // Payment addresses: + if (link.pay != null) { + const { + publicAddress, + addressType, + label, + message, + memo, + nativeAmount, + minNativeAmount, + tokenId, + expires, + isGateway + } = link.pay + out.publicAddress = publicAddress + if (addressType === 'legacyAddress') out.legacyAddress = publicAddress + if (addressType === 'segwitAddress') out.segwitAddress = publicAddress + out.metadata = { + name: label, + notes: message, + // @ts-expect-error Undocumented feature: + gateway: isGateway + } + out.uniqueIdentifier = memo + out.nativeAmount = nativeAmount + out.minNativeAmount = minNativeAmount + out.tokenId = tokenId + out.expireDate = expires + ;(out as any).gateway = isGateway + } + + // Payment protocol: + if (link.paymentProtocol != null) { + const { paymentProtocolUrl } = link.paymentProtocol + out.paymentProtocolUrl = paymentProtocolUrl + } + + // Private keys: + if (link.privateKey != null) { + const { privateKey } = link.privateKey + out.privateKeys = [privateKey] + } + + // Custom tokens: + if (link.token != null) { + out.token = makeMetaToken(link.token) + // @ts-expect-error Undocumented "ERC20" field: + out.token.type = link.token.networkLocation.type + } + + if (link.walletConnect != null) { + out.walletConnect = link.walletConnect + } + + return out +} diff --git a/src/core/currency/wallet/currency-wallet-api.ts b/src/core/currency/wallet/currency-wallet-api.ts index 55a9f02b..ea66e932 100644 --- a/src/core/currency/wallet/currency-wallet-api.ts +++ b/src/core/currency/wallet/currency-wallet-api.ts @@ -43,6 +43,7 @@ import { makeMetaTokens } from '../../account/custom-tokens' import { toApiInput } from '../../root-pixie' import { makeStorageWalletApi } from '../../storage/storage-api' import { getCurrencyMultiplier } from '../currency-selectors' +import { linkToParsedUri } from '../uri-tools' import { makeCurrencyWalletCallbacks } from './currency-wallet-callbacks' import { asEdgeAssetAction, @@ -724,31 +725,68 @@ export function makeCurrencyWalletApi( // URI handling: async encodeUri(options: EdgeEncodeUri): Promise { - return await tools.encodeUri( - options, - makeMetaTokens( - input.props.state.accounts[accountId].customTokens[pluginId] + const allTokens = + input.props.state.accounts[accountId].allTokens[pluginId] + + if (tools.encodePayLink != null) { + const { tokenId = null } = upgradeCurrencyCode({ + allTokens, + currencyInfo: plugin.currencyInfo, + currencyCode: options.currencyCode + }) + return await tools.encodePayLink( + { ...options, addressType: 'publicAddress', tokenId }, + { allTokens } ) - ) + } + + if (tools.encodeUri != null) { + return await tools.encodeUri( + options, + makeMetaTokens( + input.props.state.accounts[accountId].customTokens[pluginId] + ) + ) + } + + return '' }, + async parseUri(uri: string, currencyCode?: string): Promise { - const parsedUri = await tools.parseUri( - uri, - currencyCode, - makeMetaTokens( - input.props.state.accounts[accountId].customTokens[pluginId] - ) - ) + const allTokens = + input.props.state.accounts[accountId].allTokens[pluginId] - if (parsedUri.tokenId === undefined) { + if (tools.parseLink != null) { const { tokenId = null } = upgradeCurrencyCode({ - allTokens: input.props.state.accounts[accountId].allTokens[pluginId], + allTokens, currencyInfo: plugin.currencyInfo, - currencyCode: parsedUri.currencyCode ?? currencyCode + currencyCode }) - parsedUri.tokenId = tokenId + const out = await tools.parseLink(uri, { allTokens, tokenId }) + return linkToParsedUri(out) + } + + if (tools.parseUri != null) { + const parsedUri = await tools.parseUri( + uri, + currencyCode, + makeMetaTokens( + input.props.state.accounts[accountId].customTokens[pluginId] + ) + ) + + if (parsedUri.tokenId === undefined) { + const { tokenId = null } = upgradeCurrencyCode({ + allTokens, + currencyInfo: plugin.currencyInfo, + currencyCode: parsedUri.currencyCode ?? currencyCode + }) + parsedUri.tokenId = tokenId + } + return parsedUri } - return parsedUri + + return {} }, // Generic: diff --git a/src/types/types.ts b/src/types/types.ts index 93f69adf..ede42071 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -821,6 +821,64 @@ export interface EdgeEncodeUri { currencyCode?: string } +/** + * A payment address, with optional metadata. + */ +export interface EdgePayLink { + publicAddress: string + + /** Same meaning as EdgeAddress.addressType */ + addressType: string + + /** Recipient name */ + label?: string + + /** Transaction note */ + message?: string + + /** On-chain memo */ + memo?: string + memoType?: 'text' | 'number' | 'hex' // EdgeMemoOption['type'] + + /** Amount to send */ + nativeAmount?: string + minNativeAmount?: string + + /** What to send, specifically */ + tokenId?: EdgeTokenId + + /** If the address will go away */ + expires?: Date + + /** True if this is a Renproject Gateway URI */ + isGateway?: boolean +} + +export interface EdgePaymentProtocolLink { + paymentProtocolUrl: string +} + +/** + * A private key, with the power of spending. + */ +export interface EdgePrivateKeyLink { + privateKey: string +} + +/** + * A parsed link can have multiple meanings, + * so returns whatever interpretations make sense. + * For instance, an EVM address can be both a payment request + * and a view key. + */ +export interface EdgeParsedLink { + pay?: EdgePayLink + paymentProtocol?: EdgePaymentProtocolLink + privateKey?: EdgePrivateKeyLink + token?: EdgeToken + walletConnect?: WalletConnect +} + // options ------------------------------------------------------------- export interface EdgeTokenIdOptions { @@ -1146,12 +1204,25 @@ export interface EdgeCurrencyTools { readonly getTokenId?: (token: EdgeToken) => Promise // URIs: - readonly parseUri: ( + readonly parseLink: ( + link: string, + opts: { allTokens: EdgeTokenMap; tokenId?: EdgeTokenId } + ) => Promise + + readonly encodePayLink: ( + link: EdgePayLink, + opts: { allTokens: EdgeTokenMap } + ) => Promise + + /** @deprecated Provide encodeLink instead */ + readonly parseUri?: ( uri: string, currencyCode?: string, customTokens?: EdgeMetaToken[] ) => Promise - readonly encodeUri: ( + + /** @deprecated Provide encodeLink instead */ + readonly encodeUri?: ( obj: EdgeEncodeUri, customTokens?: EdgeMetaToken[] ) => Promise @@ -1344,16 +1415,18 @@ export interface EdgeCurrencyWallet { readonly dumpData: () => Promise readonly resyncBlockchain: () => Promise - // URI handling: + // Generic: + readonly otherMethods: EdgeOtherMethods + + /** @deprecated Use EdgeCurrencyConfig.encodePayLink instead */ readonly encodeUri: (obj: EdgeEncodeUri) => Promise + + /** @deprecated Use EdgeCurrencyConfig.parseLink instead */ readonly parseUri: ( uri: string, currencyCode?: string ) => Promise - // Generic: - readonly otherMethods: EdgeOtherMethods - /** @deprecated Use the information in EdgeCurrencyInfo / EdgeToken. */ readonly denominationToNative: ( denominatedAmount: string, @@ -1621,6 +1694,13 @@ export interface EdgeCurrencyConfig { readonly userSettings: JsonObject | undefined readonly changeUserSettings: (settings: JsonObject) => Promise + // URI handling: + readonly parseLink: ( + link: string, + opts?: { tokenId?: EdgeTokenId } + ) => Promise + readonly encodePayLink: (link: EdgePayLink) => Promise + // Utility methods: readonly importKey: ( userInput: string, diff --git a/test/fake/fake-broken-engine.ts b/test/fake/fake-broken-engine.ts index 96501506..709cf908 100644 --- a/test/fake/fake-broken-engine.ts +++ b/test/fake/fake-broken-engine.ts @@ -30,10 +30,10 @@ export const brokenEnginePlugin: EdgeCurrencyPlugin = { getSplittableTypes() { return Promise.resolve([]) }, - parseUri() { + parseLink() { return Promise.resolve({}) }, - encodeUri() { + encodePayLink() { return Promise.resolve('') } } diff --git a/test/fake/fake-currency-plugin.ts b/test/fake/fake-currency-plugin.ts index 882436d8..b86214be 100644 --- a/test/fake/fake-currency-plugin.ts +++ b/test/fake/fake-currency-plugin.ts @@ -12,7 +12,7 @@ import { EdgeFreshAddress, EdgeGetReceiveAddressOptions, EdgeGetTransactionsOptions, - EdgeParsedUri, + EdgeParsedLink, EdgeSpendInfo, EdgeStakingStatus, EdgeToken, @@ -367,11 +367,11 @@ class FakeCurrencyTools implements EdgeCurrencyTools { } // URI parsing: - parseUri(uri: string): Promise { + parseLink(uri: string): Promise { return Promise.resolve({}) } - encodeUri(): Promise { + encodePayLink(): Promise { return Promise.resolve('') } }