Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

- added: `EdgeCurrencyConfig.encodePayLink`
- added: `EdgeCurrencyConfig.parseLink`
- deprecated: `EdgeCurrencyWallet.encodeUri`. Use `EdgeCurrencyConfig.encodePayLink`
- deprecated: `EdgeCurrencyWallet.parseUri`. Use `EdgeCurrencyConfig.parseLink`

## 2.42.0 (2026-02-10)

- added: `EdgeCurrencyEngineCallbacks.onSyncStatusChanged` callback.
Expand Down
61 changes: 60 additions & 1 deletion src/core/account/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = []
Expand Down Expand Up @@ -186,6 +190,54 @@ export class CurrencyConfig
)
}

async parseLink(
link: string,
opts: { tokenId?: EdgeTokenId } = {}
): Promise<EdgeParsedLink> {
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<string> {
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 } = {}
Expand All @@ -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<EdgeSwapConfig> {
Expand Down
170 changes: 170 additions & 0 deletions src/core/currency/uri-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
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:
const payAddress = legacyAddress ?? publicAddress ?? segwitAddress
if (payAddress != null) {
out.pay = {
publicAddress: payAddress,
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,
// Plugins marked RenBridge Gateway addresses using this
// undocumented field:
isGateway: (metadata as any)?.gateway
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

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,
// The edge-currency-accountbased custom token parser would
// insert this undocumented field into `EdgeMetaToken`.
// We can preserve this information in `networkLocation`,
// which is a free-form field designed to hold info like this:
type: (token as any).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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check on networkLocation causes crash

Medium Severity

In linkToParsedUri, link.token.networkLocation.type is accessed without checking whether networkLocation is defined. EdgeToken.networkLocation is typed as JsonObject | undefined and is documented as being undefined for special built-in tokens like staking balances. If a plugin's parseLink returns an EdgeParsedLink with a token whose networkLocation is undefined, this line throws a TypeError at runtime. This function is called from the wallet's deprecated parseUri backward-compatibility path, so the crash surfaces to callers of an existing API.

Fix in Cursor Fix in Web

}

if (link.walletConnect != null) {
out.walletConnect = link.walletConnect
}

return out
}
72 changes: 55 additions & 17 deletions src/core/currency/wallet/currency-wallet-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { splitWalletInfo } from '../../login/splitting'
import { toApiInput } from '../../root-pixie'
import { makeStorageWalletApi } from '../../storage/storage-api'
import { getCurrencyMultiplier } from '../currency-selectors'
import { linkToParsedUri } from '../uri-tools'
import {
determineConfirmations,
makeCurrencyWalletCallbacks,
Expand Down Expand Up @@ -748,31 +749,68 @@ export function makeCurrencyWalletApi(

// URI handling:
async encodeUri(options: EdgeEncodeUri): Promise<string> {
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<EdgeParsedUri> {
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:
Expand Down
14 changes: 5 additions & 9 deletions src/core/login/splitting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,14 @@ export async function splitWalletInfo(
// Restore anything that has simply been deleted:
if (toRestore.length > 0) {
const newStates: EdgeWalletStates = {}
let hasChanges = false
for (const existingWalletInfo of toRestore) {
if (existingWalletInfo.archived || existingWalletInfo.deleted) {
hasChanges = true
newStates[existingWalletInfo.id] = {
archived: false,
deleted: false,
migratedFromWalletId: existingWalletInfo.migratedFromWalletId
}
newStates[existingWalletInfo.id] = {
archived: false,
deleted: false,
migratedFromWalletId: existingWalletInfo.migratedFromWalletId
}
}
if (hasChanges) await changeWalletStates(ai, accountId, newStates)
await changeWalletStates(ai, accountId, newStates)
}

// Add the keys to the login:
Expand Down
Loading
Loading