Skip to content

Commit dac176a

Browse files
committed
feat: add UTXO namespace to WalletConnect hub provider
1 parent 7b2bfaf commit dac176a

8 files changed

Lines changed: 353 additions & 1 deletion

File tree

wallets/provider-walletconnect-2/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"lint": "eslint \"**/*.{ts,tsx}\""
2222
},
2323
"dependencies": {
24+
"@bitcoinerlab/secp256k1": "^1.2.0",
2425
"@hub3js/core": "^0.60.1",
2526
"@hub3js/evm": "^0.2.1",
2627
"@hub3js/std": "^0.1.1",
@@ -34,6 +35,7 @@
3435
"@walletconnect/sign-client": "2.23.7",
3536
"@walletconnect/universal-provider": "2.23.7",
3637
"@walletconnect/utils": "2.23.7",
38+
"bitcoinjs-lib": "^6.1.7",
3739
"bs58": "^5.0.0",
3840
"caip": "^1.1.1",
3941
"rango-types": "^0.5.0"

wallets/provider-walletconnect-2/src/constants.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { ProviderMetadata } from '@hub3js/core';
22

3+
import { Networks } from '@rango-dev/wallets-shared';
34
import {
45
type BlockchainMeta,
56
type EvmBlockchainMeta,
67
isEvmBlockchain,
8+
type TransferBlockchainMeta,
79
} from 'rango-types';
810

911
import getSigners from './signer.js';
@@ -31,6 +33,16 @@ export const metadata: ProviderMetadata = {
3133
isEvmBlockchain(chain)
3234
),
3335
},
36+
{
37+
label: 'BTC',
38+
value: 'UTXO',
39+
id: 'BTC',
40+
getSupportedChains: (allBlockchains: BlockchainMeta[]) =>
41+
allBlockchains.filter(
42+
(chain): chain is TransferBlockchainMeta =>
43+
chain.name === Networks.BTC
44+
),
45+
},
3446
],
3547
},
3648
},

wallets/provider-walletconnect-2/src/legacy/signer.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ export default async function getSigners(
2525
async () => await import('../signers/solana.js')
2626
)
2727
).default;
28+
const UtxoSigner = (
29+
await dynamicImportWithRefinedError(
30+
async () => await import('../signers/utxo.js')
31+
)
32+
).default;
33+
2834
signers.registerSigner(
2935
TxType.EVM,
3036
new EVMSigner(instance.client, () => instance.getSession('evm'))
@@ -35,6 +41,10 @@ export default async function getSigners(
3541
instance.getSession('solana' as WalletConnectNamespace)
3642
)
3743
);
44+
signers.registerSigner(
45+
TxType.TRANSFER,
46+
new UtxoSigner(instance.client, () => instance.getSession('utxo'))
47+
);
3848

3949
return signers;
4050
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type { Subscriber, SubscriberCleanUp } from '@hub3js/core';
2+
import type { UtxoActions } from '@rango-dev/wallets-core/namespaces/utxo';
3+
import type { SignClientTypes } from '@walletconnect/types';
4+
5+
import {
6+
CAIP_BITCOIN_CHAIN_ID,
7+
utils,
8+
} from '@rango-dev/wallets-core/namespaces/utxo';
9+
import { AccountId } from 'caip';
10+
11+
import { BitcoinEvents, NAMESPACES } from '../../legacy/constants.js';
12+
import { getAdapter } from '../../provider.js';
13+
import {
14+
type Bip122AddressEntry,
15+
pickPaymentAddress,
16+
} from '../../session/bip122.js';
17+
import { expireWalletConnectTopic } from '../../session/teardown.js';
18+
19+
export function sessionEventSubscriber(): [
20+
Subscriber<UtxoActions>,
21+
SubscriberCleanUp<UtxoActions>
22+
] {
23+
let sessionUpdateHandler: (
24+
args: SignClientTypes.EventArguments['session_update']
25+
) => void;
26+
let sessionEventHandler: (
27+
args: SignClientTypes.EventArguments['session_event']
28+
) => void;
29+
let sessionDeleteHandler: (
30+
args: SignClientTypes.EventArguments['session_delete']
31+
) => void;
32+
33+
return [
34+
async (context) => {
35+
const adapter = getAdapter();
36+
const client = await adapter.getClient();
37+
const [, setState] = context.state();
38+
39+
sessionUpdateHandler = (args) => {
40+
const session = adapter.getSession('utxo');
41+
if (!session || args.topic !== session.topic) {
42+
return;
43+
}
44+
45+
const bip122Namespace = args.params.namespaces[NAMESPACES.BITCOIN];
46+
if (!bip122Namespace?.accounts?.length) {
47+
return;
48+
}
49+
50+
const address = new AccountId(bip122Namespace.accounts[0]).address;
51+
setState(
52+
'accounts',
53+
utils.formatAccountsToCAIP([address], CAIP_BITCOIN_CHAIN_ID)
54+
);
55+
};
56+
57+
sessionEventHandler = (args) => {
58+
const session = adapter.getSession('utxo');
59+
if (!session || args.topic !== session.topic) {
60+
return;
61+
}
62+
63+
if (args.params.event.name !== BitcoinEvents.ADDRESSES_CHANGED) {
64+
return;
65+
}
66+
67+
const data = args.params.event.data as Bip122AddressEntry[];
68+
if (!data?.length) {
69+
void context.action('disconnect');
70+
return;
71+
}
72+
73+
const paymentAddress = pickPaymentAddress(data);
74+
75+
if (!paymentAddress) {
76+
void context.action('disconnect');
77+
return;
78+
}
79+
80+
setState(
81+
'accounts',
82+
utils.formatAccountsToCAIP([paymentAddress], CAIP_BITCOIN_CHAIN_ID)
83+
);
84+
};
85+
86+
sessionDeleteHandler = (event) => {
87+
const session = adapter.getSession('utxo');
88+
if (!session || event.topic !== session.topic) {
89+
return;
90+
}
91+
92+
adapter.clearSession('utxo');
93+
void expireWalletConnectTopic(client, event.topic);
94+
context.action('disconnect');
95+
};
96+
97+
client.on('session_update', sessionUpdateHandler);
98+
client.on('session_event', sessionEventHandler);
99+
client.on('session_delete', sessionDeleteHandler);
100+
},
101+
(_, err) => {
102+
const adapter = getAdapter();
103+
void adapter.getClient().then((client) => {
104+
if (sessionUpdateHandler) {
105+
client.off('session_update', sessionUpdateHandler);
106+
}
107+
if (sessionEventHandler) {
108+
client.off('session_event', sessionEventHandler);
109+
}
110+
if (sessionDeleteHandler) {
111+
client.off('session_delete', sessionDeleteHandler);
112+
}
113+
});
114+
115+
return err;
116+
},
117+
];
118+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { ActionBuilder, NamespaceBuilder } from '@hub3js/core';
2+
import * as commonBuilders from '@hub3js/std/builders';
3+
import { standardizeAndThrowError } from '@hub3js/std/operators';
4+
import {
5+
builders,
6+
CAIP_BITCOIN_CHAIN_ID,
7+
utils,
8+
type UtxoActions,
9+
} from '@rango-dev/wallets-core/namespaces/utxo';
10+
11+
import { WALLET_ID } from '../../constants.js';
12+
import { getAdapter } from '../../provider.js';
13+
import { filterBip122Accounts } from '../../session/bip122.js';
14+
15+
import { sessionEventSubscriber } from './hooks.js';
16+
17+
const [sessionSubscriber, sessionSubscriberCleanup] = sessionEventSubscriber();
18+
19+
const connect = builders
20+
.connect()
21+
.action(async function () {
22+
const adapter = getAdapter();
23+
const session = await adapter.ensureSession({ namespace: 'utxo' });
24+
const bip122Accounts = filterBip122Accounts(session);
25+
26+
if (!bip122Accounts.length) {
27+
throw new Error('No Bitcoin accounts found in WalletConnect session.');
28+
}
29+
30+
return utils.formatAccountsToCAIP(
31+
[bip122Accounts[0].address],
32+
CAIP_BITCOIN_CHAIN_ID
33+
);
34+
})
35+
.before(sessionSubscriber)
36+
.or(sessionSubscriberCleanup)
37+
.or(standardizeAndThrowError)
38+
.build();
39+
40+
const canEagerConnect = new ActionBuilder<UtxoActions, 'canEagerConnect'>(
41+
'canEagerConnect'
42+
)
43+
.action(async () => getAdapter().tryRestoreEagerSession('utxo'))
44+
.build();
45+
46+
const disconnect = commonBuilders
47+
.disconnect<UtxoActions>()
48+
.after(() => {
49+
void getAdapter().disconnectSession('utxo');
50+
})
51+
.after(sessionSubscriberCleanup)
52+
.build();
53+
54+
const utxo = new NamespaceBuilder<UtxoActions>('UTXO', WALLET_ID)
55+
.action(connect)
56+
.action(disconnect)
57+
.action(canEagerConnect)
58+
.build();
59+
60+
export { utxo };

wallets/provider-walletconnect-2/src/provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ProviderBuilder } from '@hub3js/core';
55
import { WalletConnectAdapter } from './adapter/adapter.js';
66
import { metadata, WALLET_ID } from './constants.js';
77
import { evm } from './namespaces/evm/namespace.js';
8+
import { utxo } from './namespaces/utxo/namespace.js';
89

910
let adapter: WalletConnectAdapter;
1011

@@ -29,6 +30,7 @@ const buildProvider = () =>
2930
})
3031
.config('metadata', metadata)
3132
.add('evm', evm)
33+
.add('utxo', utxo)
3234
.build();
3335

3436
export { buildProvider };

0 commit comments

Comments
 (0)