fix(suite): fix label drop for RBG transactions

This commit is contained in:
Peter Sanderson
2023-12-11 11:53:19 +01:00
committed by Peter Sanderson
parent b4327a4bd8
commit 96d12a424c
13 changed files with 1256 additions and 15 deletions

View File

@@ -350,7 +350,13 @@ export type SuiteAnalyticsEvent =
| {
type: EventType.SettingsGeneralLabelingProvider;
payload: {
provider: 'dropbox' | 'google' | 'fileSystem' | 'sdCard' | 'missing-provider' | '';
provider:
| 'dropbox'
| 'google'
| 'fileSystem'
| 'missing-provider'
| 'inMemoryTest'
| ''; // Todo: 'sdCard' not implemented yet
};
}
| {

View File

@@ -35,6 +35,7 @@ import {
selectMetadata,
selectSelectedProviderForLabels,
} from 'src/reducers/suite/metadataReducer';
import { InMemoryTestProvider } from '../../services/suite/metadata/InMemoryTestProvider';
export const setAccountAdd = createAction(METADATA.ACCOUNT_ADD, (payload: Account) => ({
payload,
@@ -80,11 +81,12 @@ export type MetadataAction =
// needs to be declared here in top level context because it's not recommended to keep classes instances in redux state (serialization)
const providerInstance: Record<
DataType,
DropboxProvider | GoogleProvider | FileSystemProvider | undefined
DropboxProvider | GoogleProvider | FileSystemProvider | InMemoryTestProvider | undefined
> = {
labels: undefined,
passwords: undefined,
};
const fetchIntervals: { [deviceState: string]: any } = {}; // any because of native at the moment, otherwise number | undefined
const createProviderInstance = (
@@ -93,6 +95,7 @@ const createProviderInstance = (
environment: OAuthServerEnvironment = 'production',
clientId?: string,
) => {
// eslint-disable-next-line default-case
switch (type) {
case 'dropbox':
return new DropboxProvider({
@@ -103,8 +106,8 @@ const createProviderInstance = (
return new GoogleProvider(tokens, environment);
case 'fileSystem':
return new FileSystemProvider();
default:
throw new Error(`provider of type ${type} is not implemented`);
case 'inMemoryTest':
return new InMemoryTestProvider();
}
};
@@ -803,8 +806,13 @@ export const addAccountMetadata =
if (payload.type === 'outputLabel') {
if (typeof payload.value !== 'string' || payload.value.length === 0) {
if (!nextMetadata.outputLabels[payload.txid])
return Promise.resolve({ success: false });
if (!nextMetadata.outputLabels[payload.txid]) {
// If we try to delete already deleted label it's ok.
// No problem happened. ¯\_ (ツ)_/¯
return Promise.resolve({ success: true });
}
delete nextMetadata.outputLabels[payload.txid][payload.outputIndex];
if (Object.keys(nextMetadata.outputLabels[payload.txid]).length === 0) {
delete nextMetadata.outputLabels[payload.txid];

View File

@@ -0,0 +1,570 @@
import { AccountsRootState } from '@suite-common/wallet-core';
import { Account } from '@suite-common/wallet-types';
export const accountSpendingCoins: Account = {
deviceState: 'mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q@AC94BB9C1B08FE73BE1E3322:0',
index: 0,
backendType: undefined,
misc: undefined,
marker: undefined,
path: "m/84'/1'/0'",
descriptor:
'(accountSpendingCoins:descriptor)vpub5YX1yJFY8E236pH3iNvCpThsXLxoQoC4nwraaS5h4TZwaSp1Gg9SQoxCsrumxjh7nZRQQkNfH29TEDeMvAZVmD3rpmsDnFc5Sj4JgJG6m4b',
key: '(accountSpendingCoins:key)vpub5YX1yJFY8E236pH3iNvCpThsXLxoQoC4nwraaS5h4TZwaSp1Gg9SQoxCsrumxjh7nZRQQkNfH29TEDeMvAZVmD3rpmsDnFc5Sj4JgJG6m4b-regtest-mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q@AC94BB9C1B08FE73BE1E3322:0',
accountType: 'normal',
symbol: 'regtest',
empty: false,
visible: true,
balance: '199993110',
availableBalance: '199993110',
formattedBalance: '1.9999311',
tokens: [],
addresses: {
change: [
{
address: 'bcrt1qejqxwzfld7zr6mf7ygqy5s5se5xq7vmt8ntmj0',
path: "m/84'/1'/0'/1/0",
transfers: 1,
balance: '199993110',
sent: '0',
received: '199993110',
},
{
address: 'bcrt1qze76uzqteg6un6jfcryrxhwvfvjj58tsdeh9xy',
path: "m/84'/1'/0'/1/1",
transfers: 0,
},
{
address: 'bcrt1qr5p6f5sk09sms57ket074vywfymuthlg7y8tn0',
path: "m/84'/1'/0'/1/2",
transfers: 0,
},
{
address: 'bcrt1qwn0s88t9r39g72m78mcaxj72sy3ct4m4dulavf',
path: "m/84'/1'/0'/1/3",
transfers: 0,
},
{
address: 'bcrt1qguznsd2hyl69gjx2axd6f5qu9k274qj9v5syhd',
path: "m/84'/1'/0'/1/4",
transfers: 0,
},
{
address: 'bcrt1q8zx9dlztqz9apm7y5gtx8a0tlz57fhncycvun5',
path: "m/84'/1'/0'/1/5",
transfers: 0,
},
{
address: 'bcrt1qger2dlc2mykcavfxs0ad8eupr058njwp2e8n6m',
path: "m/84'/1'/0'/1/6",
transfers: 0,
},
{
address: 'bcrt1qjm4p4nykvsczt26llswppmfe7xraane9nfruqv',
path: "m/84'/1'/0'/1/7",
transfers: 0,
},
{
address: 'bcrt1qm3sx7jlgj7yd3y2ad0jm587k98pcc2x5wfq5jf',
path: "m/84'/1'/0'/1/8",
transfers: 0,
},
{
address: 'bcrt1qcvgld0z38vnx9fnpsgwuc583838ldv8sf38pwz',
path: "m/84'/1'/0'/1/9",
transfers: 0,
},
{
address: 'bcrt1quja53ex7clst8yhkwenhy8p67aa36kedqszlun',
path: "m/84'/1'/0'/1/10",
transfers: 0,
},
{
address: 'bcrt1qlxvp7fwy0azketw0afgw0snxssyphmtthe4g8g',
path: "m/84'/1'/0'/1/11",
transfers: 0,
},
{
address: 'bcrt1ql02lgacsfm543teeymtw2p7xz9unxe6572mxeh',
path: "m/84'/1'/0'/1/12",
transfers: 0,
},
{
address: 'bcrt1qnuafw94yvu6td7tcfqea823y342ttrc9d32qnx',
path: "m/84'/1'/0'/1/13",
transfers: 0,
},
{
address: 'bcrt1q92khcru77q7hrctf7ter2kltpgtrz23nhnkcaz',
path: "m/84'/1'/0'/1/14",
transfers: 0,
},
{
address: 'bcrt1qu8ts4a80ylfq7hgy9aqt0rk65gekmt5p029ymm',
path: "m/84'/1'/0'/1/15",
transfers: 0,
},
{
address: 'bcrt1qc0zg0xrs0grvrmr0hrq7u0rdthadsrk406w0dk',
path: "m/84'/1'/0'/1/16",
transfers: 0,
},
{
address: 'bcrt1quvm4wfs9pjrqvr4rmy60k8uevg009jhtx9zzxr',
path: "m/84'/1'/0'/1/17",
transfers: 0,
},
{
address: 'bcrt1qetngs772lxz97x94v9ycfjtwelmu7aqmuu5vkl',
path: "m/84'/1'/0'/1/18",
transfers: 0,
},
{
address: 'bcrt1qaydzjvcf0qnfxs9aw8zmazt88f6j0wjtgqqn22',
path: "m/84'/1'/0'/1/19",
transfers: 0,
},
{
address: 'bcrt1qlecqdxptwt65c52uqzmdqk9njyt4rmygf2vsvd',
path: "m/84'/1'/0'/1/20",
transfers: 0,
},
],
used: [
{
address: 'bcrt1qkvwu9g3k2pdxewfqr7syz89r3gj557l374sg5v',
path: "m/84'/1'/0'/0/0",
transfers: 2,
balance: '0',
sent: '1000000000',
received: '1000000000',
},
{
address: 'bcrt1qldlynaqp0hy4zc2aag3pkenzvxy65saej0huey',
path: "m/84'/1'/0'/0/1",
transfers: 2,
balance: '0',
sent: '1000000000',
received: '1000000000',
},
],
unused: [
{
address: 'bcrt1q9l0rk0gkgn73d0gc57qn3t3cwvucaj3h98jwg4',
path: "m/84'/1'/0'/0/2",
transfers: 0,
},
{
address: 'bcrt1qtxe2hdle9he8hc2xds7yl2m8zutjksv0gmszw2',
path: "m/84'/1'/0'/0/3",
transfers: 0,
},
{
address: 'bcrt1qglrv8xrtf68udd5pxj2pxyq5s7lynq204v2da8',
path: "m/84'/1'/0'/0/4",
transfers: 0,
},
{
address: 'bcrt1qds6ygc07t7d8prjs60qnx0nv4gexx9hex07wwl',
path: "m/84'/1'/0'/0/5",
transfers: 0,
},
{
address: 'bcrt1q86udlgffezp9kgjvqlfah7a6c8dpepameugfw5',
path: "m/84'/1'/0'/0/6",
transfers: 0,
},
{
address: 'bcrt1q503m8pxyvf7ypurcvwv2kp0ajyjumsjq5ad7xq',
path: "m/84'/1'/0'/0/7",
transfers: 0,
},
{
address: 'bcrt1qg805w4uhsz3sy9stasdx2rkwp4haf446ew055v',
path: "m/84'/1'/0'/0/8",
transfers: 0,
},
{
address: 'bcrt1qy2f6mkfa3aaecqz2s2xr0utf6edza7qzh7gnnn',
path: "m/84'/1'/0'/0/9",
transfers: 0,
},
{
address: 'bcrt1q4tm6cgxd3m7uqgzmwxfclruqz894qdv5w05kss',
path: "m/84'/1'/0'/0/10",
transfers: 0,
},
{
address: 'bcrt1qguvdun4cjty8js34wswdn4nv2ne7jamajjwgkj',
path: "m/84'/1'/0'/0/11",
transfers: 0,
},
{
address: 'bcrt1qp6py2d8acmqcvdfeht3escetv5aunru5d4vlk0',
path: "m/84'/1'/0'/0/12",
transfers: 0,
},
{
address: 'bcrt1q88fkyl4zxrcejt4s75ynkunpps3n9kchzvnaw8',
path: "m/84'/1'/0'/0/13",
transfers: 0,
},
{
address: 'bcrt1qjfvfjqsp3khtgdkqw87up39skp6zvp062q7y93',
path: "m/84'/1'/0'/0/14",
transfers: 0,
},
{
address: 'bcrt1qf49m5zxk9957z8yzyfed6glrcvz3r7y4demdex',
path: "m/84'/1'/0'/0/15",
transfers: 0,
},
{
address: 'bcrt1qfmej7qk4f66vx8a5aq5t5nlvp0hxuwe0fg9rrp',
path: "m/84'/1'/0'/0/16",
transfers: 0,
},
{
address: 'bcrt1qlfxf67wwud59ru0d4e7qa36zh0daxcfr6ec53g',
path: "m/84'/1'/0'/0/17",
transfers: 0,
},
{
address: 'bcrt1qcmmen68dyt59pkh7dv2xxf07tme7qxzn0upszf',
path: "m/84'/1'/0'/0/18",
transfers: 0,
},
{
address: 'bcrt1qpcgw9fuec7wjjnq8rl0cwfwa7mqvrheud24890',
path: "m/84'/1'/0'/0/19",
transfers: 0,
},
{
address: 'bcrt1qxukydnhdldsjf0c8rguxmja9kxsydjzwj486hk',
path: "m/84'/1'/0'/0/20",
transfers: 0,
},
{
address: 'bcrt1qq7tkmktggcwp46jefmnh7ytade562ka8qte4un',
path: "m/84'/1'/0'/0/21",
transfers: 0,
},
],
},
utxo: [
{
txid: '4ec6d03abb2e3e52a50301ee353621cbda13d7a6cb57bc8f373519af5b25026b(originalTransaction)',
vout: 0,
amount: '199993110',
blockHeight: 153,
address: 'bcrt1qejqxwzfld7zr6mf7ygqy5s5se5xq7vmt8ntmj0',
path: "m/84'/1'/0'/1/0",
confirmations: 1,
},
],
history: { total: 3, unconfirmed: 0, addrTxCount: 5 },
metadata: {
'1': {
fileName: '7061500d8482d422b07cbd59784db51ae60f72a5c25f47d3d344888585e0c37d.mtdt',
aesKey: '16994717c3c3a64fefd0725e4e4599826ea82bdc1b56c3cf664541c83ccef7d4',
},
key: 'tpubDCZB6sR48s4T5Cr8qHUYSZEFCQMMHRg8AoVKVmvcAP5bRw7ArDKeoNwKAJujV3xCPkBvXH5ejSgbgyN6kREmF7sMd41NdbuHa8n1DZNxSMg',
},
networkType: 'bitcoin',
page: { index: 1, size: 25, total: 1 },
};
export const accountReceivingCoins: Account = {
deviceState: 'mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q@AC94BB9C1B08FE73BE1E3322:0',
index: 1,
backendType: undefined,
misc: undefined,
marker: undefined,
path: "m/84'/1'/1'",
descriptor:
'(accountReceivingCoins:descriptor)vpub5YX1yJFY8E238aESifzcpXQHLzNDYJC22yLWqCwJ5pN85E27ku5wUXdhnh3HSMs3HibDQzeWmVeH52bAAa9LvkK4L1V9XfZbmHxGDuZSJks',
key: '(accountReceivingCoins:key)vpub5YX1yJFY8E238aESifzcpXQHLzNDYJC22yLWqCwJ5pN85E27ku5wUXdhnh3HSMs3HibDQzeWmVeH52bAAa9LvkK4L1V9XfZbmHxGDuZSJks-regtest-mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q@AC94BB9C1B08FE73BE1E3322:0',
accountType: 'normal',
symbol: 'regtest',
empty: false,
visible: true,
balance: '1799997910',
availableBalance: '1799997910',
formattedBalance: '17.9999791',
tokens: [],
addresses: {
change: [
{
address: 'bcrt1q757q50q0mgt3xkqfneflvutpyktxv3x3jfupg8',
path: "m/84'/1'/1'/1/0",
transfers: 1,
balance: '99997910',
sent: '0',
received: '99997910',
},
{
address: 'bcrt1q5k7s87q26syf6muqthlprn06sxhklq693u69lu',
path: "m/84'/1'/1'/1/1",
transfers: 0,
},
{
address: 'bcrt1qaugvv7tvw6h434feqs4ep62qxv7e0xdm9txmft',
path: "m/84'/1'/1'/1/2",
transfers: 0,
},
{
address: 'bcrt1qrm08dns67sj0ycvsqm0rnqzp7ff2crdgurmjzd',
path: "m/84'/1'/1'/1/3",
transfers: 0,
},
{
address: 'bcrt1qnrk8ewh48ynxrqlvtmaepgxax3623rx043zate',
path: "m/84'/1'/1'/1/4",
transfers: 0,
},
{
address: 'bcrt1qz4xx7glecua7fy0xvdvwx3vhh4zpa7pyfw7dhy',
path: "m/84'/1'/1'/1/5",
transfers: 0,
},
{
address: 'bcrt1qr2fm62qd8r9thyvkn44drxr0877jgy88xukyam',
path: "m/84'/1'/1'/1/6",
transfers: 0,
},
{
address: 'bcrt1qhzhpctls7v8l2cvxnhz743m3lkpptq4twq6wu9',
path: "m/84'/1'/1'/1/7",
transfers: 0,
},
{
address: 'bcrt1qcg80e2vyv52vl8fx5842y2raxx4lp4f5wg85dm',
path: "m/84'/1'/1'/1/8",
transfers: 0,
},
{
address: 'bcrt1qtayc24uac0hm95eyvmz4rcalnhj46hasutljcy',
path: "m/84'/1'/1'/1/9",
transfers: 0,
},
{
address: 'bcrt1qrspwywt63gp07uwn7f5n0522v4jxnhglpskzjl',
path: "m/84'/1'/1'/1/10",
transfers: 0,
},
{
address: 'bcrt1qjkgrmaffye33epk5p4cy67ynnhj4njml0j4lmu',
path: "m/84'/1'/1'/1/11",
transfers: 0,
},
{
address: 'bcrt1qq7n68efyh97v7fvdkq8eh7thnlaag4z7kpme7g',
path: "m/84'/1'/1'/1/12",
transfers: 0,
},
{
address: 'bcrt1qc0qc6wjds0mlw29zu5rn7ngkqu5p6rnkp2zdeu',
path: "m/84'/1'/1'/1/13",
transfers: 0,
},
{
address: 'bcrt1q8pdptz6j4z7ky5e264sjcf9c4g6ushvlc75m7l',
path: "m/84'/1'/1'/1/14",
transfers: 0,
},
{
address: 'bcrt1qp4fqac7tqmtsxuef7sgud90xljyza2ffaqm5a2',
path: "m/84'/1'/1'/1/15",
transfers: 0,
},
{
address: 'bcrt1q5a4vteamypse527xxm9995h6vempc8jps5ttq6',
path: "m/84'/1'/1'/1/16",
transfers: 0,
},
{
address: 'bcrt1qx2t5x8ce6wvdc5t9ax04rf2ruudd84f6lmaysq',
path: "m/84'/1'/1'/1/17",
transfers: 0,
},
{
address: 'bcrt1qrcdafxz8wtjlxcryzddulrqe2mtgjz38t8s9ha',
path: "m/84'/1'/1'/1/18",
transfers: 0,
},
{
address: 'bcrt1qvw8wmaz2qvfkcuz9fm7dyy4yzuy6evle7z8uaj',
path: "m/84'/1'/1'/1/19",
transfers: 0,
},
{
address: 'bcrt1qnt0rdkhrzd5550ca57ja0zjxr9uq57ydpm3tzw',
path: "m/84'/1'/1'/1/20",
transfers: 0,
},
],
used: [
{
address: 'bcrt1q7r9yvcdgcl6wmtta58yxf29a8kc96jkyyk8fsw',
path: "m/84'/1'/1'/0/0",
transfers: 2,
balance: '0',
sent: '900000000',
received: '900000000',
},
{
address: 'bcrt1qq80deypyenykvjm6sws5522937u344vuhjk4qq',
path: "m/84'/1'/1'/0/1",
transfers: 2,
balance: '0',
sent: '900000000',
received: '900000000',
},
{
address: 'bcrt1q6p7we05ktaksrp9c3m0rgnheqstdaljdx9gjaf',
path: "m/84'/1'/1'/0/2",
transfers: 1,
balance: '1700000000',
sent: '0',
received: '1700000000',
},
],
unused: [
{
address: 'bcrt1qs3krye0f3hhknsf9wudy82zkmp5j35rs9wjg33',
path: "m/84'/1'/1'/0/3",
transfers: 0,
},
{
address: 'bcrt1qpdq7f5vv90ydwjjzc0r5z5c6yp7qhrj4r8egfy',
path: "m/84'/1'/1'/0/4",
transfers: 0,
},
{
address: 'bcrt1qlksrfcwqlzlphpdsglh2cj00efayz2nh846v6x',
path: "m/84'/1'/1'/0/5",
transfers: 0,
},
{
address: 'bcrt1ql47490ve5j98wltlar03a25v4lupjgqhrg4dp8',
path: "m/84'/1'/1'/0/6",
transfers: 0,
},
{
address: 'bcrt1q87dfkaegfr3aqqda2t5kr3a04pah67gckx5stl',
path: "m/84'/1'/1'/0/7",
transfers: 0,
},
{
address: 'bcrt1qtusttyvgpaaneh2c78fqejm48g9j95sux0a9rz',
path: "m/84'/1'/1'/0/8",
transfers: 0,
},
{
address: 'bcrt1qanwed8uyhlm33hya47ps6kv9sl6kuh45hdt6m6',
path: "m/84'/1'/1'/0/9",
transfers: 0,
},
{
address: 'bcrt1qcmrfzuenh02ntrv28t0acm4mwc23u0lan9mzdt',
path: "m/84'/1'/1'/0/10",
transfers: 0,
},
{
address: 'bcrt1qajh9yx9hwppjskfndmpcs5vdu02an0pfgvhu5m',
path: "m/84'/1'/1'/0/11",
transfers: 0,
},
{
address: 'bcrt1qswtgqxygvj8kqfagqrj45lnjutuzec4qz2js27',
path: "m/84'/1'/1'/0/12",
transfers: 0,
},
{
address: 'bcrt1qg0n0mwxg5frrrzqaclgzur6qkc4q8my64wu09g',
path: "m/84'/1'/1'/0/13",
transfers: 0,
},
{
address: 'bcrt1qcm66dfrtjnpg7vte7pwnt45jvct4jmckjth8yk',
path: "m/84'/1'/1'/0/14",
transfers: 0,
},
{
address: 'bcrt1qvgntsus4syjj9zehr7240l90ese76kwfg5rkt3',
path: "m/84'/1'/1'/0/15",
transfers: 0,
},
{
address: 'bcrt1qvcxpmklkjjn7f4rpuqf92tvtk4mlllgke4hurp',
path: "m/84'/1'/1'/0/16",
transfers: 0,
},
{
address: 'bcrt1qa464t8hqjk86f2z0xt2v064jut4w8zstdvmpfp',
path: "m/84'/1'/1'/0/17",
transfers: 0,
},
{
address: 'bcrt1qaxhydk5qcaj3ctqy0uktc85e534lzqqvz97v3n',
path: "m/84'/1'/1'/0/18",
transfers: 0,
},
{
address: 'bcrt1qcwcue88vje7k6cz0xcdm6wkrkxv06ask6gqeah',
path: "m/84'/1'/1'/0/19",
transfers: 0,
},
{
address: 'bcrt1qlvfg066zcyvp0l5mn9sf8p62j9lsyzm8ur4s05',
path: "m/84'/1'/1'/0/20",
transfers: 0,
},
{
address: 'bcrt1qn9czsla64h9dq3mtqmwwhznwsvq3892hj7gkg0',
path: "m/84'/1'/1'/0/21",
transfers: 0,
},
{
address: 'bcrt1qtg5atnpz4mh3e9px2a8jlqu0j66jql26vf0lr2',
path: "m/84'/1'/1'/0/22",
transfers: 0,
},
],
},
utxo: [
{
txid: '46c84bcd1ef55ea64dab2853e577dffb26f31022fbf49c18981f2c186a3a2d80(chainSpendingReceivedCoins)',
vout: 0,
amount: '99997910',
blockHeight: 153,
address: 'bcrt1q757q50q0mgt3xkqfneflvutpyktxv3x3jfupg8',
path: "m/84'/1'/1'/1/0",
confirmations: 1,
},
{
txid: '46c84bcd1ef55ea64dab2853e577dffb26f31022fbf49c18981f2c186a3a2d80(chainSpendingReceivedCoins)',
vout: 1,
amount: '1700000000',
blockHeight: 153,
address: 'bcrt1q6p7we05ktaksrp9c3m0rgnheqstdaljdx9gjaf',
path: "m/84'/1'/1'/0/2",
confirmations: 1,
},
],
history: { total: 2, unconfirmed: 0, addrTxCount: 6 },
metadata: {
'1': {
fileName: '803c346fae1cac3580afdf34c480d0691b57bce10f4fccf671ecbf902c9c7b20.mtdt',
aesKey: '29ed09b52c608ebcc162d3e83e51ac788e3a580937d66f42b34cb8e9e74b997b',
},
key: 'tpubDCZB6sR48s4T6xoXqaYxScvf23kmQvg5QpyFkYnDBjsmviKHLSG9s6cp593Exg87tuMjXXMWDvBRXnJtzppcQf8Z8HdJP1rothfxm4qnPXo',
},
networkType: 'bitcoin',
page: { index: 1, size: 25, total: 1 },
};
export const moveLabelsForRbfAccountsFixture: AccountsRootState['wallet']['accounts'] = [
accountSpendingCoins,
accountReceivingCoins,
];

View File

@@ -0,0 +1,49 @@
import { MetadataState } from '@suite-common/metadata-types';
import {
originalTransactionSpendAccount,
chainSpendingReceivedCoins,
} from './moveLabelsForRbfTransactions.fixture';
export const moveLabelsForRbfMetadataStateFixture: MetadataState = {
enabled: true,
initiating: false,
providers: [
{
type: 'inMemoryTest',
isCloud: false,
tokens: {},
user: '',
clientId: '',
data: {
'7061500d8482d422b07cbd59784db51ae60f72a5c25f47d3d344888585e0c37d.mtdt': {
accountLabel: '',
outputLabels: {
[originalTransactionSpendAccount.txid]: {
'1': '1A',
'2': '1B',
},
},
addressLabels: {},
},
'803c346fae1cac3580afdf34c480d0691b57bce10f4fccf671ecbf902c9c7b20.mtdt': {
accountLabel: '',
outputLabels: {
[originalTransactionSpendAccount.txid]: {
'1': '2A',
'2': '2B',
},
[chainSpendingReceivedCoins.txid]: {
'1': '2C',
},
},
addressLabels: {},
},
},
},
],
selectedProvider: {
labels: '',
passwords: '',
},
error: {},
};

View File

@@ -0,0 +1,267 @@
import { TransactionsRootState } from '@suite-common/wallet-core';
import { WalletAccountTransaction } from '@suite-common/wallet-types';
import { accountReceivingCoins, accountSpendingCoins } from './moveLabelsForRbfAccounts.fixture';
export const originalTransactionSpendAccount: WalletAccountTransaction = {
descriptor: accountSpendingCoins.descriptor,
deviceState: 'mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q@AC94BB9C1B08FE73BE1E3322:0',
symbol: 'regtest',
type: 'sent',
txid: '4ec6d03abb2e3e52a50301ee353621cbda13d7a6cb57bc8f373519af5b25026b(originalTransaction)',
blockTime: 1702460477,
blockHeight: undefined,
blockHash: '2aa61579eee0e8a8263361c63c1632281a0b88af496fa9d1ff50380aa7931bc9',
amount: '1800000000',
fee: '6890',
vsize: 240,
feeRate: '28.71',
targets: [
{
n: 1,
addresses: ['bcrt1qq80deypyenykvjm6sws5522937u344vuhjk4qq'],
isAddress: true,
amount: '900000000',
},
{
n: 2,
addresses: ['bcrt1q7r9yvcdgcl6wmtta58yxf29a8kc96jkyyk8fsw'],
isAddress: true,
amount: '900000000',
},
],
tokens: [],
internalTransfers: [],
rbf: true,
details: {
vin: [
{
txid: '17b3aa686f4da9b5e12cdb556d277fe407fb4b3be7c571c527a53309824f76f7',
sequence: 4294967293,
n: 0,
addresses: ['bcrt1qldlynaqp0hy4zc2aag3pkenzvxy65saej0huey'],
isAddress: true,
isOwn: true,
value: '1000000000',
isAccountOwned: true,
},
{
txid: '1f10a72efb295d2161800fce9ae6496dfa23f1f08f816a76b4dcae34102f88f2',
vout: 1,
sequence: 4294967293,
n: 1,
addresses: ['bcrt1qkvwu9g3k2pdxewfqr7syz89r3gj557l374sg5v'],
isAddress: true,
isOwn: true,
value: '1000000000',
isAccountOwned: true,
},
],
vout: [
{
value: '199993110',
n: 0,
hex: '0014cc8067093f6f843d6d3e22004a4290cd0c0f336b',
addresses: ['bcrt1qejqxwzfld7zr6mf7ygqy5s5se5xq7vmt8ntmj0'],
isAddress: true,
isOwn: true,
isAccountOwned: true,
},
{
value: '900000000',
n: 1,
spent: true,
hex: '001401dedc9024ccc9664b7a83a14a29458fb91ad59c',
addresses: ['bcrt1qq80deypyenykvjm6sws5522937u344vuhjk4qq'],
isAddress: true,
},
{
value: '900000000',
n: 2,
spent: true,
hex: '0014f0ca4661a8c7f4edad7da1c864a8bd3db05d4ac4',
addresses: ['bcrt1q7r9yvcdgcl6wmtta58yxf29a8kc96jkyyk8fsw'],
isAddress: true,
},
],
size: 402,
totalInput: '2000000000',
totalOutput: '1999993110',
},
};
export const transactionSendingCoinsReplacement: WalletAccountTransaction = {
...originalTransactionSpendAccount,
txid: 'fd763c2bdf671f896a83b95d46a66c886812e1908abbf68540bb218be818868d(replacementTransaction)',
};
export const originalTransactionReceivingAccount: WalletAccountTransaction = {
descriptor: accountReceivingCoins.descriptor,
deviceState: 'mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q@AC94BB9C1B08FE73BE1E3322:0',
symbol: 'regtest',
type: 'recv',
txid: '4ec6d03abb2e3e52a50301ee353621cbda13d7a6cb57bc8f373519af5b25026b(originalTransaction)',
blockTime: 1702460477,
blockHeight: undefined,
blockHash: '2aa61579eee0e8a8263361c63c1632281a0b88af496fa9d1ff50380aa7931bc9',
amount: '1800000000',
fee: '6890',
vsize: 240,
feeRate: '28.71',
targets: [
{
n: 1,
addresses: ['bcrt1qq80deypyenykvjm6sws5522937u344vuhjk4qq'],
isAddress: true,
amount: '900000000',
isAccountTarget: true,
},
{
n: 2,
addresses: ['bcrt1q7r9yvcdgcl6wmtta58yxf29a8kc96jkyyk8fsw'],
isAddress: true,
amount: '900000000',
isAccountTarget: true,
},
],
tokens: [],
internalTransfers: [],
rbf: true,
details: {
vin: [
{
txid: '17b3aa686f4da9b5e12cdb556d277fe407fb4b3be7c571c527a53309824f76f7',
sequence: 4294967293,
n: 0,
addresses: ['bcrt1qldlynaqp0hy4zc2aag3pkenzvxy65saej0huey'],
isAddress: true,
value: '1000000000',
},
{
txid: '1f10a72efb295d2161800fce9ae6496dfa23f1f08f816a76b4dcae34102f88f2',
vout: 1,
sequence: 4294967293,
n: 1,
addresses: ['bcrt1qkvwu9g3k2pdxewfqr7syz89r3gj557l374sg5v'],
isAddress: true,
value: '1000000000',
},
],
vout: [
{
value: '199993110',
n: 0,
hex: '0014cc8067093f6f843d6d3e22004a4290cd0c0f336b',
addresses: ['bcrt1qejqxwzfld7zr6mf7ygqy5s5se5xq7vmt8ntmj0'],
isAddress: true,
},
{
value: '900000000',
n: 1,
spent: true,
hex: '001401dedc9024ccc9664b7a83a14a29458fb91ad59c',
addresses: ['bcrt1qq80deypyenykvjm6sws5522937u344vuhjk4qq'],
isAddress: true,
isOwn: true,
isAccountOwned: true,
},
{
value: '900000000',
n: 2,
spent: true,
hex: '0014f0ca4661a8c7f4edad7da1c864a8bd3db05d4ac4',
addresses: ['bcrt1q7r9yvcdgcl6wmtta58yxf29a8kc96jkyyk8fsw'],
isAddress: true,
isOwn: true,
isAccountOwned: true,
},
],
size: 402,
totalInput: '2000000000',
totalOutput: '1999993110',
},
};
export const chainSpendingReceivedCoins: WalletAccountTransaction = {
descriptor: accountReceivingCoins.descriptor,
deviceState: 'mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q@AC94BB9C1B08FE73BE1E3322:0',
symbol: 'regtest',
type: 'self',
txid: '46c84bcd1ef55ea64dab2853e577dffb26f31022fbf49c18981f2c186a3a2d80(chainSpendingReceivedCoins)',
blockTime: 1702460477,
blockHeight: undefined,
blockHash: '2aa61579eee0e8a8263361c63c1632281a0b88af496fa9d1ff50380aa7931bc9',
amount: '2090',
fee: '2090',
vsize: 209,
feeRate: '10',
targets: [
{
n: 1,
addresses: ['bcrt1q6p7we05ktaksrp9c3m0rgnheqstdaljdx9gjaf'],
isAddress: true,
amount: '1700000000',
isAccountTarget: true,
},
],
tokens: [],
internalTransfers: [],
rbf: true,
details: {
vin: [
{
txid: '4ec6d03abb2e3e52a50301ee353621cbda13d7a6cb57bc8f373519af5b25026b(originalTransaction)',
vout: 1,
sequence: 4294967293,
n: 0,
addresses: ['bcrt1qq80deypyenykvjm6sws5522937u344vuhjk4qq'],
isAddress: true,
isOwn: true,
value: '900000000',
isAccountOwned: true,
},
{
txid: '4ec6d03abb2e3e52a50301ee353621cbda13d7a6cb57bc8f373519af5b25026b(originalTransaction)',
vout: 2,
sequence: 4294967293,
n: 1,
addresses: ['bcrt1q7r9yvcdgcl6wmtta58yxf29a8kc96jkyyk8fsw'],
isAddress: true,
isOwn: true,
value: '900000000',
isAccountOwned: true,
},
],
vout: [
{
value: '99997910',
n: 0,
hex: '0014f53c0a3c0fda171358099e53f6716125966644d1',
addresses: ['bcrt1q757q50q0mgt3xkqfneflvutpyktxv3x3jfupg8'],
isAddress: true,
isOwn: true,
isAccountOwned: true,
},
{
value: '1700000000',
n: 1,
hex: '0014d07cecbe965f6d0184b88ede344ef90416defe4d',
addresses: ['bcrt1q6p7we05ktaksrp9c3m0rgnheqstdaljdx9gjaf'],
isAddress: true,
isOwn: true,
isAccountOwned: true,
},
],
size: 372,
totalInput: '1800000000',
totalOutput: '1799997910',
},
};
export const moveLabelsForRbfTransactionsFixture: TransactionsRootState['wallet']['transactions']['transactions'] =
{
[accountSpendingCoins.key]: [originalTransactionSpendAccount],
[accountReceivingCoins.key]: [
originalTransactionReceivingAccount,
chainSpendingReceivedCoins,
],
};

View File

@@ -0,0 +1,105 @@
import { combineReducers } from '@reduxjs/toolkit';
import { accountsReducer, transactionsReducer } from '../../../reducers/wallet';
import { configureMockStore, initPreloadedState } from '@suite-common/test-utils';
import { findLabelsToBeMovedOrDeleted, moveLabelsForRbfAction } from '../moveLabelsForRbfActions';
import {
accountReceivingCoins,
accountSpendingCoins,
moveLabelsForRbfAccountsFixture,
} from '../__fixtures__/moveLabelsForRbf/moveLabelsForRbfAccounts.fixture';
import {
moveLabelsForRbfTransactionsFixture,
originalTransactionSpendAccount,
transactionSendingCoinsReplacement,
} from '../__fixtures__/moveLabelsForRbf/moveLabelsForRbfTransactions.fixture';
import metadataReducer, {
selectLabelingDataForAccount,
} from '../../../reducers/suite/metadataReducer';
import { moveLabelsForRbfMetadataStateFixture } from '../__fixtures__/moveLabelsForRbf/moveLabelsForRbfMetadataState.fixture';
import suiteReducer from '../../../reducers/suite/suiteReducer';
const rootReducer = combineReducers({
wallet: combineReducers({
accounts: accountsReducer,
transactions: transactionsReducer,
}),
metadata: metadataReducer,
suite: suiteReducer,
});
type TestState = ReturnType<typeof rootReducer>;
const initStore = ({
wallet,
metadata,
}: {
wallet: TestState['wallet'];
metadata: TestState['metadata'];
}) => {
// State != suite AppState, therefore <any>
const store = configureMockStore<any>({
reducer: rootReducer,
preloadedState: initPreloadedState({
rootReducer,
partialState: {
wallet,
metadata,
},
}),
});
return store;
};
describe(moveLabelsForRbfAction.name, () => {
it('moves the labels onto new RBF transaction and deletes the label of the chained transaction', async () => {
const store = initStore({
wallet: {
accounts: moveLabelsForRbfAccountsFixture,
transactions: {
isLoading: false,
error: null,
transactions: moveLabelsForRbfTransactionsFixture,
},
},
metadata: moveLabelsForRbfMetadataStateFixture,
});
const toBeMovedOrDeletedList = store.dispatch(
findLabelsToBeMovedOrDeleted({
prevTxid: originalTransactionSpendAccount.txid,
}),
);
await store.dispatch(
moveLabelsForRbfAction({
toBeMovedOrDeletedList,
newTxid: transactionSendingCoinsReplacement.txid,
}),
);
const accountSpendingCoinsMetadata = selectLabelingDataForAccount(
store.getState(),
accountSpendingCoins.key,
);
expect(accountSpendingCoinsMetadata.outputLabels).toStrictEqual({
[transactionSendingCoinsReplacement.txid]: {
'1': '1A',
'2': '1B',
},
});
const accountReceivingCoinsMetadata = selectLabelingDataForAccount(
store.getState(),
accountReceivingCoins.key,
);
expect(accountReceivingCoinsMetadata.outputLabels).toStrictEqual({
[transactionSendingCoinsReplacement.txid]: {
'1': '2A',
'2': '2B',
},
});
});
});

View File

@@ -0,0 +1,149 @@
import { selectLabelingDataForAccount } from '../../reducers/suite/metadataReducer';
import { findChainedTransactions, findTransactions } from '@suite-common/wallet-utils';
import { Dispatch, GetState } from 'src/types/suite';
import * as metadataActions from 'src/actions/suite/metadataActions';
import { AccountLabels, AccountOutputLabels } from '@suite-common/metadata-types';
import { AccountKey, WalletAccountTransaction } from '@suite-common/wallet-types';
type DeleteAllOutputLabelsParams = {
labels: AccountLabels['outputLabels']['labels'];
dispatch: Dispatch;
accountKey: AccountKey;
txid: string;
};
const deleteDanglingLabels = async ({
labels,
dispatch,
accountKey,
txid,
}: DeleteAllOutputLabelsParams) => {
// eslint-disable-next-line no-restricted-syntax
for (const outputIndex of Object.keys(labels)) {
// eslint-disable-next-line no-await-in-loop
await dispatch(
metadataActions.addMetadata({
type: 'outputLabel',
entityKey: accountKey,
txid,
outputIndex: Number(outputIndex),
defaultValue: '',
value: '',
}),
);
}
};
type MoveLabelToNewTransactionParams = {
accountOutputLabels: AccountOutputLabels;
dispatch: Dispatch;
accountKey: AccountKey;
newTxid: string;
};
export const copyLabelToNewTransaction = async ({
accountOutputLabels,
dispatch,
accountKey,
newTxid,
}: MoveLabelToNewTransactionParams) => {
// eslint-disable-next-line no-restricted-syntax,
for (const outputIndex of Object.keys(accountOutputLabels)) {
const value = accountOutputLabels[outputIndex];
// eslint-disable-next-line no-await-in-loop
await dispatch(
metadataActions.addMetadata({
type: 'outputLabel',
entityKey: accountKey,
txid: newTxid,
outputIndex: Number(outputIndex),
defaultValue: '',
value,
}),
);
}
};
type FindLabelsToBeMovedOrDeletedParams = {
prevTxid: string;
};
export type LabelsToBeMovedOrDeleted = Record<
AccountKey,
{
toBeMoved: WalletAccountTransaction;
toBeDeleted: WalletAccountTransaction[];
}
>;
export const findLabelsToBeMovedOrDeleted =
({ prevTxid }: FindLabelsToBeMovedOrDeletedParams) =>
(_dispatch: Dispatch, getState: GetState): LabelsToBeMovedOrDeleted => {
const accountTransactions = findTransactions(
prevTxid,
getState().wallet.transactions.transactions,
);
return accountTransactions.reduce((result, accountTransaction) => {
const chainedTransactionsToDrop = findChainedTransactions(
accountTransaction.tx.descriptor,
accountTransaction.tx.txid,
getState().wallet.transactions.transactions,
);
const allAccountsTransactionsIncludingChained: WalletAccountTransaction[] = [
accountTransaction.tx,
...(chainedTransactionsToDrop?.own ?? []),
// Intentionally using `chainedTransactionsToDrop?.others`, they will be found when we query another account in the loop
];
result[accountTransaction.key] = {
toBeDeleted: allAccountsTransactionsIncludingChained,
toBeMoved: accountTransaction.tx,
};
return result;
}, {} as LabelsToBeMovedOrDeleted);
};
type MoveLabelsForRbfParams = {
newTxid: string;
toBeMovedOrDeletedList: LabelsToBeMovedOrDeleted;
};
export const moveLabelsForRbfAction =
({ toBeMovedOrDeletedList, newTxid }: MoveLabelsForRbfParams) =>
async (dispatch: Dispatch, getState: GetState) => {
// eslint-disable-next-line no-restricted-syntax
for (const toBeMovedOrDeleted of Object.entries(toBeMovedOrDeletedList)) {
const [accountKey, data] = toBeMovedOrDeleted;
const accountMetadata = selectLabelingDataForAccount(getState(), accountKey);
const accountOutputLabelsToBeMoved: AccountOutputLabels =
accountMetadata?.outputLabels?.[data.toBeMoved.txid] ?? {};
// eslint-disable-next-line no-await-in-loop
await copyLabelToNewTransaction({
accountKey,
accountOutputLabels: accountOutputLabelsToBeMoved,
newTxid,
dispatch,
});
// eslint-disable-next-line no-restricted-syntax
for (const transactionToDrop of data.toBeDeleted) {
const accountOutputLabelsToBeDeleted: AccountOutputLabels =
accountMetadata?.outputLabels?.[transactionToDrop.txid] ?? {};
const deleteParams: DeleteAllOutputLabelsParams = {
accountKey,
dispatch,
labels: accountOutputLabelsToBeDeleted,
txid: transactionToDrop.txid,
};
// eslint-disable-next-line no-await-in-loop
await deleteDanglingLabels(deleteParams);
}
}
};

View File

@@ -26,7 +26,7 @@ import {
PrecomposedTransactionFinal,
PrecomposedTransactionFinalCardano,
} from '@suite-common/wallet-types';
import { cloneObject } from '@trezor/utils';
import { cloneObject, getSynchronize } from '@trezor/utils';
import * as modalActions from 'src/actions/suite/modalActions';
import * as metadataActions from 'src/actions/suite/metadataActions';
@@ -40,6 +40,7 @@ import * as sendFormEthereumActions from './send/sendFormEthereumActions';
import * as sendFormRippleActions from './send/sendFormRippleActions';
import * as sendFormSolanaActions from './send/sendFormSolanaActions';
import * as sendFormCardanoActions from './send/sendFormCardanoActions';
import { findLabelsToBeMovedOrDeleted, moveLabelsForRbfAction } from './moveLabelsForRbfActions';
export type SendFormAction =
| {
@@ -213,6 +214,12 @@ const pushTransaction =
const device = selectDevice(getState());
if (!signedTx || !precomposedTx || !account) return;
const isRbf = precomposedTx.prevTxid !== undefined;
const toBeMovedOrDeletedList = isRbf
? dispatch(findLabelsToBeMovedOrDeleted({ prevTxid: precomposedTx.prevTxid }))
: undefined;
const sentTx = await TrezorConnect.pushTransaction(signedTx);
// const sentTx = { success: true, payload: { txid: 'ABC ' } };
@@ -250,7 +257,16 @@ const pushTransaction =
}),
);
if (precomposedTx.prevTxid) {
if (isRbf) {
if (toBeMovedOrDeletedList !== undefined) {
await dispatch(
moveLabelsForRbfAction({
toBeMovedOrDeletedList,
newTxid: txid,
}),
);
}
// notification from the backend may be delayed.
// modify affected transaction(s) in the reducer until the real account update occurs.
// this will update transaction details (like time, fee etc.)
@@ -317,6 +333,8 @@ const pushTransaction =
outputsPermutation = precomposedTx?.outputsPermutation;
}
const synchronize = getSynchronize();
precomposedForm?.outputs
// create array of metadata objects
.map((formOutput, index) => {
@@ -340,7 +358,10 @@ const pushTransaction =
// propagate metadata to reducers and persistent storage
.forEach((output, index, arr) => {
const isLast = index === arr.length - 1;
dispatch(metadataActions.addAccountMetadata(output, isLast));
synchronize(() =>
dispatch(metadataActions.addAccountMetadata(output, isLast)),
);
});
}
} else {

View File

@@ -21,6 +21,7 @@ import {
} from 'src/actions/suite/constants/metadataConstants';
import { SuiteRootState } from './suiteReducer';
import { AccountKey } from '@suite-common/wallet-types';
export const initialState: MetadataState = {
// is Suite trying to load metadata (get master key -> sync cloud)?
@@ -141,7 +142,7 @@ export const selectLabelingDataForSelectedAccount = (state: {
*/
export const selectLabelingDataForAccount = (
state: { metadata: MetadataState; wallet: { accounts: Account[] } },
accountKey: string,
accountKey: AccountKey,
) => {
const provider = selectSelectedProviderForLabels(state);
const account = selectAccountByKey(state, accountKey);

View File

@@ -0,0 +1,58 @@
import { AbstractMetadataProvider } from 'src/types/suite/metadata';
export class InMemoryTestProvider extends AbstractMetadataProvider {
#files: Record<string, string> = {};
isCloud = false;
constructor() {
super('inMemoryTest');
}
get clientId() {
return this.type;
}
connect() {
return Promise.resolve(this.ok());
}
disconnect() {
return Promise.resolve(this.ok());
}
// eslint-disable-next-line
async getProviderDetails() {
return this.ok({
type: this.type,
isCloud: this.isCloud,
tokens: {},
user: '',
clientId: this.clientId,
});
}
getFileContent(file: string) {
return Promise.resolve(this.ok(Buffer.from(this.#files[file], 'hex')));
}
setFileContent(file: string, content: Buffer) {
this.#files[file] = content.toString('hex');
return Promise.resolve(this.ok(undefined));
}
getFilesList() {
return Promise.resolve(this.ok(Object.keys(this.#files)));
}
renameFile(from: string, to: string) {
this.#files[to] = this.#files[from];
delete this.#files[from];
return Promise.resolve(this.ok(undefined));
}
isConnected() {
return true;
}
}

View File

@@ -26,3 +26,5 @@ export const getSynchronize = () => {
return lock;
};
};
export type Synchronize = ReturnType<typeof getSynchronize>;

View File

@@ -50,7 +50,7 @@ export type MetadataAddPayload =
// }
export type MetadataItem = string;
export type MetadataProviderType = 'dropbox' | 'google' | 'fileSystem' | 'sdCard';
export type MetadataProviderType = 'dropbox' | 'google' | 'fileSystem' | 'inMemoryTest'; // Todo: | 'sdCard'
export type Tokens = {
accessToken?: string;
@@ -176,9 +176,11 @@ export abstract class AbstractMetadataProvider {
}
}
export type AccountOutputLabels = { [index: string]: MetadataItem };
export interface AccountLabels {
accountLabel?: MetadataItem;
outputLabels: { [txid: string]: { [index: string]: MetadataItem } };
outputLabels: { [txid: string]: AccountOutputLabels };
addressLabels: { [address: string]: MetadataItem };
}

View File

@@ -8,6 +8,7 @@ import {
WalletAccountTransaction,
ChainedTransactions,
PrecomposedTransactionFinal,
AccountKey,
} from '@suite-common/wallet-types';
import {
AccountAddress,
@@ -254,14 +255,14 @@ export const findTransactions = (
export const findChainedTransactions = (
descriptor: string,
txid: string,
transactions: Record<string, WalletAccountTransaction[]>,
transactions: Record<AccountKey, WalletAccountTransaction[]>,
result: ChainedTransactions = { own: [], others: [] },
) => {
Object.keys(transactions).forEach(key => {
Object.keys(transactions).forEach(accountKey => {
const ownTxs = result.own.map(tx => tx.txid);
const othersTxs = result.others.map(tx => tx.txid);
// check if any pending transaction is using the utxo/vin with requested txid
const txs = transactions[key].filter(tx => {
const txs = transactions[accountKey].filter(tx => {
if (!isPending(tx) || !tx.details.vin.find(i => i.txid === txid)) {
return false;
}
@@ -273,10 +274,12 @@ export const findChainedTransactions = (
result.own.push(tx);
return true;
}
if (!ownTxs.includes(tx.txid) && !othersTxs.includes(tx.txid)) {
result.others.push(tx);
return true;
}
return false;
});