@rango-dev/provider-ctrl 0.62.1-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +319 -0
  2. package/dist/actions/utxo.d.ts +17 -0
  3. package/dist/actions/utxo.d.ts.map +1 -0
  4. package/dist/builders/utxo.d.ts +21 -0
  5. package/dist/builders/utxo.d.ts.map +1 -0
  6. package/dist/constants.d.ts +23 -0
  7. package/dist/constants.d.ts.map +1 -0
  8. package/dist/mod.d.ts +3 -0
  9. package/dist/mod.d.ts.map +1 -0
  10. package/dist/mod.js +2 -0
  11. package/dist/mod.js.map +7 -0
  12. package/dist/namespaces/evm.d.ts +4 -0
  13. package/dist/namespaces/evm.d.ts.map +1 -0
  14. package/dist/namespaces/solana.d.ts +4 -0
  15. package/dist/namespaces/solana.d.ts.map +1 -0
  16. package/dist/namespaces/utxo.d.ts +4 -0
  17. package/dist/namespaces/utxo.d.ts.map +1 -0
  18. package/dist/provider-ctrl.build.json +1 -0
  19. package/dist/provider.d.ts +3 -0
  20. package/dist/provider.d.ts.map +1 -0
  21. package/dist/signer.d.ts +4 -0
  22. package/dist/signer.d.ts.map +1 -0
  23. package/dist/signers/solana.d.ts +13 -0
  24. package/dist/signers/solana.d.ts.map +1 -0
  25. package/dist/signers/utxo.d.ts +17 -0
  26. package/dist/signers/utxo.d.ts.map +1 -0
  27. package/dist/types.d.ts +14 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/utils.d.ts +25 -0
  30. package/dist/utils.d.ts.map +1 -0
  31. package/package.json +37 -0
  32. package/readme.md +80 -0
  33. package/src/actions/utxo.ts +32 -0
  34. package/src/builders/utxo.ts +46 -0
  35. package/src/constants.ts +84 -0
  36. package/src/mod.ts +8 -0
  37. package/src/namespaces/evm.ts +50 -0
  38. package/src/namespaces/solana.ts +39 -0
  39. package/src/namespaces/utxo.ts +44 -0
  40. package/src/provider.ts +24 -0
  41. package/src/signer.ts +28 -0
  42. package/src/signers/solana.ts +22 -0
  43. package/src/signers/utxo.ts +167 -0
  44. package/src/types.ts +18 -0
  45. package/src/utils.ts +141 -0
@@ -0,0 +1,167 @@
1
+ import type { Provider, ProviderObject } from '../types.js';
2
+ import type { ProviderAPI as UtxoProviderApi } from '@rango-dev/wallets-core/namespaces/utxo';
3
+ import type { GenericSigner, Transfer } from 'rango-types';
4
+
5
+ import { LegacyNetworks } from '@rango-dev/wallets-core/legacy';
6
+ import { SignerError, SignerErrorCode } from 'rango-types';
7
+
8
+ import { SUPPORTED_UTXO_CHAINS } from '../constants.js';
9
+
10
+ interface CtrlTransferParams {
11
+ asset: { chain: string; symbol: string; ticker: string };
12
+ from: string;
13
+ amount: { amount: string; decimals: number };
14
+ memo?: string;
15
+ recipient?: string;
16
+ }
17
+
18
+ /** Callback-style transfer for non-PSBT UTXO chains (LTC/DOGE/BCH). */
19
+ async function ctrlTransfer(
20
+ blockchain: string,
21
+ ticker: string,
22
+ from: string,
23
+ amount: string,
24
+ decimals: number,
25
+ recipientAddress: string | null,
26
+ provider: UtxoProviderApi,
27
+ method: string,
28
+ memo?: string
29
+ ): Promise<string> {
30
+ return new Promise((resolve, reject) => {
31
+ const params: CtrlTransferParams = {
32
+ asset: { chain: blockchain, symbol: ticker, ticker },
33
+ from,
34
+ amount: { amount, decimals },
35
+ memo,
36
+ };
37
+ if (recipientAddress) {
38
+ params.recipient = recipientAddress;
39
+ }
40
+
41
+ provider.request(
42
+ { method, params: [params] },
43
+ (error: unknown, result: unknown) => {
44
+ if (error) {
45
+ reject(
46
+ new SignerError(SignerErrorCode.SEND_TX_ERROR, undefined, error)
47
+ );
48
+ } else {
49
+ resolve(result as string);
50
+ }
51
+ }
52
+ );
53
+ });
54
+ }
55
+
56
+ /**
57
+ * One signer for all of Ctrl's UTXO chains. BTC is signed via PSBT (`sign_psbt`);
58
+ * LTC/DOGE/BCH use the generic `transfer` request. It receives the whole provider
59
+ * map and resolves the right per-chain instance per transaction.
60
+ */
61
+ export class CustomTransferSigner implements GenericSigner<Transfer> {
62
+ private provider: Provider;
63
+ constructor(provider: Provider) {
64
+ this.provider = provider;
65
+ }
66
+
67
+ async signMessage(): Promise<string> {
68
+ throw SignerError.UnimplementedError('signMessage');
69
+ }
70
+
71
+ async signAndSendTx(tx: Transfer): Promise<{ hash: string }> {
72
+ const { blockchain } = tx.asset;
73
+
74
+ if (!SUPPORTED_UTXO_CHAINS.includes(blockchain)) {
75
+ throw new Error(
76
+ `blockchain: ${blockchain} transfer not implemented yet.`
77
+ );
78
+ }
79
+
80
+ if (blockchain === LegacyNetworks.BTC) {
81
+ return this.#signPsbt(tx);
82
+ }
83
+
84
+ return this.#signTransferObject(tx);
85
+ }
86
+
87
+ // https://developers.ctrl.xyz/developers/extension-bitcoin#sign-psbt-partially-signed-bitcoin-transaction
88
+ async #signPsbt(tx: Transfer): Promise<{ hash: string }> {
89
+ const { asset, psbt } = tx;
90
+
91
+ if (!psbt) {
92
+ throw new Error(
93
+ 'No PSBT found to sign. Ensure a valid PSBT is provided.'
94
+ );
95
+ }
96
+
97
+ const provider = this.provider.get(
98
+ asset.blockchain as keyof ProviderObject
99
+ ) as UtxoProviderApi;
100
+
101
+ const signInputs: { [key: string]: number[] } = {};
102
+ psbt.inputsToSign.forEach((input) => {
103
+ signInputs[input.address] = input.signingIndexes;
104
+ });
105
+
106
+ const response = await provider
107
+ .request({
108
+ method: 'sign_psbt',
109
+ /*
110
+ * Ctrl expects `params` as an array (same as its other RPCs); passing a bare
111
+ * object makes the extension read `psbt` off `undefined`. The docs showing a
112
+ * plain object are wrong.
113
+ */
114
+ params: [
115
+ {
116
+ psbt: psbt.unsignedPsbtBase64,
117
+ signInputs,
118
+ allowedSignHash: 1,
119
+ broadcast: true,
120
+ },
121
+ ],
122
+ })
123
+ .catch((error: unknown) => {
124
+ throw new SignerError(SignerErrorCode.SEND_TX_ERROR, undefined, error);
125
+ });
126
+
127
+ if (response.status === 'success') {
128
+ return { hash: response.result.txId };
129
+ }
130
+
131
+ throw new Error(
132
+ 'The operation (sign and broadcast) failed on your wallet.',
133
+ { cause: response }
134
+ );
135
+ }
136
+
137
+ async #signTransferObject(tx: Transfer): Promise<{ hash: string }> {
138
+ const { blockchain } = tx.asset;
139
+
140
+ const transferProvider = this.provider.get(
141
+ blockchain as keyof ProviderObject
142
+ ) as UtxoProviderApi;
143
+
144
+ const {
145
+ method,
146
+ memo,
147
+ recipientAddress,
148
+ decimals,
149
+ amount,
150
+ fromWalletAddress: from,
151
+ asset,
152
+ } = tx;
153
+
154
+ const hash = await ctrlTransfer(
155
+ blockchain,
156
+ asset.ticker,
157
+ from,
158
+ amount,
159
+ decimals,
160
+ recipientAddress,
161
+ transferProvider,
162
+ method,
163
+ memo ?? undefined
164
+ );
165
+ return { hash };
166
+ }
167
+ }
package/src/types.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { ProviderAPI as EvmProviderApi } from '@hub3js/evm';
2
+ import type { ProviderAPI as SolanaProviderApi } from '@hub3js/solana';
3
+ import type { LegacyNetworks } from '@rango-dev/wallets-core/legacy';
4
+ import type { ProviderAPI as UtxoProviderApi } from '@rango-dev/wallets-core/namespaces/utxo';
5
+
6
+ export type ProviderObject = {
7
+ [LegacyNetworks.ETHEREUM]: EvmProviderApi;
8
+ [LegacyNetworks.SOLANA]: SolanaProviderApi;
9
+ [LegacyNetworks.BTC]: UtxoProviderApi;
10
+ [LegacyNetworks.LTC]: UtxoProviderApi;
11
+ [LegacyNetworks.DOGE]: UtxoProviderApi;
12
+ [LegacyNetworks.BCH]: UtxoProviderApi;
13
+ };
14
+
15
+ export type Provider = Map<
16
+ keyof ProviderObject,
17
+ ProviderObject[keyof ProviderObject]
18
+ >;
package/src/utils.ts ADDED
@@ -0,0 +1,141 @@
1
+ import type { Provider } from './types.js';
2
+ import type { ProviderAPI as EvmProviderApi } from '@hub3js/evm';
3
+ import type { ProviderAPI as SolanaProviderApi } from '@hub3js/solana';
4
+ import type { CaipAccount } from '@hub3js/std/types';
5
+ import type { ProviderAPI as UtxoProviderApi } from '@rango-dev/wallets-core/namespaces/utxo';
6
+
7
+ import { LegacyNetworks } from '@rango-dev/wallets-core/legacy';
8
+ import { utils } from '@rango-dev/wallets-core/namespaces/utxo';
9
+
10
+ import { UTXO_CHAINS } from './constants.js';
11
+
12
+ export function ctrl(): Provider | null {
13
+ const { ctrl } = window;
14
+
15
+ if (!ctrl) {
16
+ return null;
17
+ }
18
+
19
+ const instances: Provider = new Map();
20
+
21
+ if (ctrl.ethereum) {
22
+ instances.set(LegacyNetworks.ETHEREUM, ctrl.ethereum);
23
+ }
24
+ if (ctrl.bitcoin) {
25
+ instances.set(LegacyNetworks.BTC, ctrl.bitcoin);
26
+ }
27
+ if (ctrl.litecoin) {
28
+ instances.set(LegacyNetworks.LTC, ctrl.litecoin);
29
+ }
30
+ if (ctrl.dogecoin) {
31
+ instances.set(LegacyNetworks.DOGE, ctrl.dogecoin);
32
+ }
33
+ if (ctrl.bitcoincash) {
34
+ instances.set(LegacyNetworks.BCH, ctrl.bitcoincash);
35
+ }
36
+ if (ctrl.solana) {
37
+ instances.set(LegacyNetworks.SOLANA, ctrl.solana);
38
+ }
39
+
40
+ if (instances.size === 0) {
41
+ return null;
42
+ }
43
+
44
+ return instances;
45
+ }
46
+
47
+ export function getInstanceOrThrow(): Provider {
48
+ const instances = ctrl();
49
+
50
+ if (!instances) {
51
+ throw new Error('Ctrl is not injected. Please check your wallet.');
52
+ }
53
+
54
+ return instances;
55
+ }
56
+
57
+ export function evmCtrl(): EvmProviderApi {
58
+ const instances = ctrl();
59
+ const evmInstance = instances?.get(LegacyNetworks.ETHEREUM);
60
+
61
+ if (!evmInstance) {
62
+ throw new Error(
63
+ 'Ctrl not injected or EVM not enabled. Please check your wallet.'
64
+ );
65
+ }
66
+
67
+ return evmInstance as EvmProviderApi;
68
+ }
69
+
70
+ export function solanaCtrl(): SolanaProviderApi {
71
+ const instances = ctrl();
72
+ const solanaInstance = instances?.get(LegacyNetworks.SOLANA);
73
+
74
+ if (!solanaInstance) {
75
+ throw new Error(
76
+ 'Ctrl not injected or Solana not enabled. Please check your wallet.'
77
+ );
78
+ }
79
+
80
+ return solanaInstance;
81
+ }
82
+
83
+ /**
84
+ * The EVM instance, used as the trigger source for UTXO account changes.
85
+ *
86
+ * Ctrl switches the active account across every chain at once but only signals it
87
+ * reliably through the EVM provider's `accountsChanged` (the UTXO providers emit an
88
+ * empty `{}` payload, and re-fetching them while disconnected opens a wallet popup).
89
+ * Returns an empty object when EVM isn't injected so the subscriber attaches safely.
90
+ */
91
+ export function evmEventSource(): EvmProviderApi {
92
+ return (ctrl()?.get(LegacyNetworks.ETHEREUM) ?? {}) as EvmProviderApi;
93
+ }
94
+
95
+ /** Promisified `request_accounts` for a callback-style ctrl UTXO instance. */
96
+ async function requestUtxoAccounts(
97
+ instance: UtxoProviderApi
98
+ ): Promise<string[]> {
99
+ return new Promise((resolve, reject) => {
100
+ instance.request(
101
+ { method: 'request_accounts', params: [] },
102
+ (error: unknown, accounts: unknown) => {
103
+ if (error) {
104
+ reject(error);
105
+ return;
106
+ }
107
+ resolve((accounts as string[]) ?? []);
108
+ }
109
+ );
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Fetch and CAIP-format accounts across every available UTXO chain, merged into a
115
+ * single array. Each account encodes its own chain via its CAIP reference, so
116
+ * BTC/LTC/DOGE/BCH addresses coexist in one UTXO namespace. Silent while the wallet
117
+ * is connected (only called on connect / switch, never on disconnect).
118
+ */
119
+ export async function getAllUtxoAccounts(): Promise<CaipAccount[]> {
120
+ const instances = ctrl();
121
+ if (!instances) {
122
+ return [];
123
+ }
124
+
125
+ /*
126
+ * `Promise.all` rejects as soon as any chain's `request_accounts` fails, discarding
127
+ * the rest and propagating that error — we don't want to silently return a partial
128
+ * account set if one chain errors.
129
+ */
130
+ const perChain = await Promise.all(
131
+ UTXO_CHAINS.filter(({ network }) => instances.get(network)).map(
132
+ async ({ network, caip }) => {
133
+ const instance = instances.get(network) as UtxoProviderApi;
134
+ const accounts = await requestUtxoAccounts(instance);
135
+ return utils.formatAccountsToCAIP(accounts, caip);
136
+ }
137
+ )
138
+ );
139
+
140
+ return perChain.flat();
141
+ }