@droplinked_inc/wallet-connection 0.1.1
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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/THREAT_MODEL.md +180 -0
- package/dist/chains.d.ts +25 -0
- package/dist/chains.d.ts.map +1 -0
- package/dist/chains.js +153 -0
- package/dist/chains.js.map +1 -0
- package/dist/connectors/evm.d.ts +144 -0
- package/dist/connectors/evm.d.ts.map +1 -0
- package/dist/connectors/evm.js +330 -0
- package/dist/connectors/evm.js.map +1 -0
- package/dist/connectors/phantom.d.ts +167 -0
- package/dist/connectors/phantom.d.ts.map +1 -0
- package/dist/connectors/phantom.js +340 -0
- package/dist/connectors/phantom.js.map +1 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +70 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +74 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +210 -0
- package/dist/provider.js.map +1 -0
- package/dist/session.d.ts +68 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +99 -0
- package/dist/session.js.map +1 -0
- package/dist/signing.d.ts +94 -0
- package/dist/signing.d.ts.map +1 -0
- package/dist/signing.js +178 -0
- package/dist/signing.js.map +1 -0
- package/dist/types.d.ts +234 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +150 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVM connector — replaces the v1.0.1 EVMProvider class.
|
|
3
|
+
*
|
|
4
|
+
* Hardening deltas:
|
|
5
|
+
* 1. No remote fetch of contract address (was a single point of supply-chain
|
|
6
|
+
* compromise via apiv3dev.droplinked.com). Caller supplies the address.
|
|
7
|
+
* 2. Wallet selection is strict: MetaMask must self-identify as MetaMask
|
|
8
|
+
* and Coinbase must self-identify as Coinbase. No fallthrough to the
|
|
9
|
+
* umbrella `window.ethereum`.
|
|
10
|
+
* 3. All RPC responses are zod-validated.
|
|
11
|
+
* 4. Login is EIP-712 typed, not personal_sign of a static string.
|
|
12
|
+
* 5. No recursive self-call in `handleWallet` (the original had an
|
|
13
|
+
* uncontrolled recursion that could spin a wallet-permissions popup
|
|
14
|
+
* loop).
|
|
15
|
+
* 6. Bigints throughout — no `any` for `totalPrice`.
|
|
16
|
+
*/
|
|
17
|
+
import { Chain, ChainWallet, Network, type ChainProvider, type EvmAddress, type IChainPayment } from '../types.js';
|
|
18
|
+
import { type Eip1193Provider } from '../provider.js';
|
|
19
|
+
import type { Hex } from 'viem';
|
|
20
|
+
/** Minimal ABI fragments — kept inline rather than fetched remotely. */
|
|
21
|
+
declare const ERC20_TRANSFER_ABI: readonly [{
|
|
22
|
+
readonly constant: false;
|
|
23
|
+
readonly inputs: readonly [{
|
|
24
|
+
readonly name: "_to";
|
|
25
|
+
readonly type: "address";
|
|
26
|
+
}, {
|
|
27
|
+
readonly name: "_value";
|
|
28
|
+
readonly type: "uint256";
|
|
29
|
+
}];
|
|
30
|
+
readonly name: "transfer";
|
|
31
|
+
readonly outputs: readonly [{
|
|
32
|
+
readonly name: "";
|
|
33
|
+
readonly type: "bool";
|
|
34
|
+
}];
|
|
35
|
+
readonly stateMutability: "nonpayable";
|
|
36
|
+
readonly type: "function";
|
|
37
|
+
}];
|
|
38
|
+
export interface EvmConnectorOptions {
|
|
39
|
+
readonly chain: Chain;
|
|
40
|
+
readonly network: Network;
|
|
41
|
+
readonly wallet?: ChainWallet;
|
|
42
|
+
/** Required for `payment()` — the droplinked checkout contract address. */
|
|
43
|
+
readonly checkoutContractAddress?: EvmAddress;
|
|
44
|
+
/** Required for typed-data signing. Defaults to globalThis.location.origin. */
|
|
45
|
+
readonly origin?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare class EvmConnector implements ChainProvider {
|
|
48
|
+
readonly chain: Chain;
|
|
49
|
+
readonly network: Network;
|
|
50
|
+
address: EvmAddress;
|
|
51
|
+
wallet: ChainWallet;
|
|
52
|
+
private readonly checkoutContractAddress;
|
|
53
|
+
private readonly origin;
|
|
54
|
+
constructor(opts: EvmConnectorOptions);
|
|
55
|
+
setAddress(address: string): this;
|
|
56
|
+
setWallet(wallet: ChainWallet): this;
|
|
57
|
+
/** Resolve the EIP-1193 provider for the configured wallet. */
|
|
58
|
+
getWalletProvider(): Eip1193Provider;
|
|
59
|
+
/**
|
|
60
|
+
* Ensure the wallet is connected to the expected address and chain.
|
|
61
|
+
* Unlike v1.0.1 this is iterative (max 3 retries) — there is no
|
|
62
|
+
* recursive self-call that can spin a popup loop.
|
|
63
|
+
*/
|
|
64
|
+
handleWallet(expectedAddress: string): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Connect + EIP-712 typed login. Returns the signature; the caller is
|
|
67
|
+
* expected to submit it to the droplinked auth API.
|
|
68
|
+
*/
|
|
69
|
+
walletLogin(): Promise<{
|
|
70
|
+
address: EvmAddress;
|
|
71
|
+
signature: string;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* ERC-20 token transfer.
|
|
75
|
+
*
|
|
76
|
+
* NOTE: This issues a direct `transfer` call. We intentionally do *not*
|
|
77
|
+
* issue an `approve` against an unbounded spender — the drainer-style
|
|
78
|
+
* "approve max uint256 to a router" pattern is rejected at the API
|
|
79
|
+
* boundary. Callers wanting allowance flows must use a different
|
|
80
|
+
* specialized package.
|
|
81
|
+
*/
|
|
82
|
+
paymentWithToken(receiver: string, amount: bigint, tokenAddress: string): Promise<string>;
|
|
83
|
+
/**
|
|
84
|
+
* Droplinked checkout contract call. Requires
|
|
85
|
+
* `checkoutContractAddress` to be supplied at construction time —
|
|
86
|
+
* we will never fetch it from a remote endpoint.
|
|
87
|
+
*/
|
|
88
|
+
payment(data: IChainPayment): Promise<{
|
|
89
|
+
deploy_hash: string;
|
|
90
|
+
cryptoAmount: bigint;
|
|
91
|
+
}>;
|
|
92
|
+
/**
|
|
93
|
+
* Submit pre-built calldata produced server-side. The calldata is
|
|
94
|
+
* NOT trusted blindly — we enforce two boundary checks before
|
|
95
|
+
* surfacing the transaction to the wallet:
|
|
96
|
+
*
|
|
97
|
+
* 1. `to` MUST equal the connector's configured
|
|
98
|
+
* `checkoutContractAddress`. Sending arbitrary calldata to an
|
|
99
|
+
* arbitrary destination would resurrect the drainer pathway
|
|
100
|
+
* that motivated removing the v1.0.1 remote-ABI fetch.
|
|
101
|
+
* 2. The first 4 bytes of `data` (the function selector) MUST be
|
|
102
|
+
* on the explicit allowlist below. Notably the selectors for
|
|
103
|
+
* `approve(address,uint256)`, `setApprovalForAll(...)`, NFT
|
|
104
|
+
* `safeTransferFrom(...)`, and ERC-20 `permit(...)` are
|
|
105
|
+
* rejected — these are the canonical "drainer" entry points
|
|
106
|
+
* that a compromised checkout API or consumer-side XSS would
|
|
107
|
+
* try to coerce the user into signing.
|
|
108
|
+
*
|
|
109
|
+
* Both checks throw `InvalidTransactionError` BEFORE any wallet
|
|
110
|
+
* prompt appears, so the user is never shown a malicious payload.
|
|
111
|
+
*/
|
|
112
|
+
submitRawTransaction(args: {
|
|
113
|
+
readonly to: EvmAddress;
|
|
114
|
+
readonly data: Hex;
|
|
115
|
+
readonly value: bigint;
|
|
116
|
+
readonly gasLimit?: bigint;
|
|
117
|
+
}): Promise<Hex>;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 4-byte function selectors that the droplinked checkout flow is known
|
|
121
|
+
* to call. Anything else is rejected at the boundary.
|
|
122
|
+
*
|
|
123
|
+
* - `0xa9059cbb` — ERC-20 `transfer(address,uint256)`
|
|
124
|
+
* The standard token payment path used by `paymentWithToken`.
|
|
125
|
+
* - `0x6a627842` — droplinked legacy checkout `droplinkedPurchase(...)`
|
|
126
|
+
* Selector matches the calldata blob produced by the droplinked
|
|
127
|
+
* checkout API. Replace if the API rotates to a new function.
|
|
128
|
+
* - `0x` (empty data) — native-currency transfer to the checkout
|
|
129
|
+
* contract. Allowed because there is no selector to spoof; the
|
|
130
|
+
* destination is already pinned by the `to === checkoutContract`
|
|
131
|
+
* check.
|
|
132
|
+
*/
|
|
133
|
+
export declare const SUBMIT_RAW_TX_SELECTOR_ALLOWLIST: ReadonlySet<string>;
|
|
134
|
+
/**
|
|
135
|
+
* Drainer selectors callers will sometimes ask the wallet to sign.
|
|
136
|
+
* Listed explicitly so the rejection message names them — when one of
|
|
137
|
+
* these appears in calldata, that is *strong* evidence that the
|
|
138
|
+
* checkout API or the consumer page is compromised.
|
|
139
|
+
*/
|
|
140
|
+
export declare const KNOWN_DRAINER_SELECTORS: Readonly<Record<string, string>>;
|
|
141
|
+
/** Returns the calldata blob for `transfer(address,uint256)`. */
|
|
142
|
+
export declare function encodeErc20Transfer(to: EvmAddress, amount: bigint): Hex;
|
|
143
|
+
export { ERC20_TRANSFER_ABI };
|
|
144
|
+
//# sourceMappingURL=evm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evm.d.ts","sourceRoot":"","sources":["../../src/connectors/evm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EACL,KAAK,EAEL,WAAW,EAEX,OAAO,EACP,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAQrB,OAAO,EAOL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAEhC,wEAAwE;AACxE,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;EAYd,CAAC;AAEX,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,2EAA2E;IAC3E,QAAQ,CAAC,uBAAuB,CAAC,EAAE,UAAU,CAAC;IAC9C,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,YAAa,YAAW,aAAa;IAChD,SAAgB,KAAK,EAAE,KAAK,CAAC;IAC7B,SAAgB,OAAO,EAAE,OAAO,CAAC;IAC1B,OAAO,EAAE,UAAU,CAAgD;IACnE,MAAM,EAAE,WAAW,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAyB;IACjE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,IAAI,EAAE,mBAAmB;IAQrC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKjC,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAUpC,+DAA+D;IAC/D,iBAAiB,IAAI,eAAe;IAUpC;;;;OAIG;IACG,YAAY,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoD1D;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,UAAU,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IA8BxE;;;;;;;;OAQG;IACG,gBAAgB,CACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC;IA0BlB;;;;OAIG;IACG,OAAO,CACX,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IAoBzD;;;;;;;;;;;;;;;;;;;OAmBG;IACG,oBAAoB,CAAC,IAAI,EAAE;QAC/B,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC;QACxB,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC;QACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;KAC5B,GAAG,OAAO,CAAC,GAAG,CAAC;CAkCjB;AAMD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gCAAgC,EAAE,WAAW,CAAC,MAAM,CAG/D,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOpE,CAAC;AAyCF,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,CASvE;AAGD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVM connector — replaces the v1.0.1 EVMProvider class.
|
|
3
|
+
*
|
|
4
|
+
* Hardening deltas:
|
|
5
|
+
* 1. No remote fetch of contract address (was a single point of supply-chain
|
|
6
|
+
* compromise via apiv3dev.droplinked.com). Caller supplies the address.
|
|
7
|
+
* 2. Wallet selection is strict: MetaMask must self-identify as MetaMask
|
|
8
|
+
* and Coinbase must self-identify as Coinbase. No fallthrough to the
|
|
9
|
+
* umbrella `window.ethereum`.
|
|
10
|
+
* 3. All RPC responses are zod-validated.
|
|
11
|
+
* 4. Login is EIP-712 typed, not personal_sign of a static string.
|
|
12
|
+
* 5. No recursive self-call in `handleWallet` (the original had an
|
|
13
|
+
* uncontrolled recursion that could spin a wallet-permissions popup
|
|
14
|
+
* loop).
|
|
15
|
+
* 6. Bigints throughout — no `any` for `totalPrice`.
|
|
16
|
+
*/
|
|
17
|
+
import { ChainPaymentSchema, ChainWallet, EvmAddressSchema, } from '../types.js';
|
|
18
|
+
import { AccountChangedException, ChainMismatchError, InvalidTransactionError, WalletNotFoundException, } from '../errors.js';
|
|
19
|
+
import { getChainMetadata } from '../chains.js';
|
|
20
|
+
import { getAccounts, getChainId, isWalletConnected, requestAccounts, selectCoinbaseProvider, selectMetaMaskProvider, } from '../provider.js';
|
|
21
|
+
import { buildLoginPayload, signLoginPayload, } from '../signing.js';
|
|
22
|
+
/** Minimal ABI fragments — kept inline rather than fetched remotely. */
|
|
23
|
+
const ERC20_TRANSFER_ABI = [
|
|
24
|
+
{
|
|
25
|
+
constant: false,
|
|
26
|
+
inputs: [
|
|
27
|
+
{ name: '_to', type: 'address' },
|
|
28
|
+
{ name: '_value', type: 'uint256' },
|
|
29
|
+
],
|
|
30
|
+
name: 'transfer',
|
|
31
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
32
|
+
stateMutability: 'nonpayable',
|
|
33
|
+
type: 'function',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
export class EvmConnector {
|
|
37
|
+
chain;
|
|
38
|
+
network;
|
|
39
|
+
address = '0x0000000000000000000000000000000000000000';
|
|
40
|
+
wallet;
|
|
41
|
+
checkoutContractAddress;
|
|
42
|
+
origin;
|
|
43
|
+
constructor(opts) {
|
|
44
|
+
this.chain = opts.chain;
|
|
45
|
+
this.network = opts.network;
|
|
46
|
+
this.wallet = opts.wallet ?? ChainWallet.Metamask;
|
|
47
|
+
this.checkoutContractAddress = opts.checkoutContractAddress;
|
|
48
|
+
this.origin = opts.origin ?? resolveOrigin();
|
|
49
|
+
}
|
|
50
|
+
setAddress(address) {
|
|
51
|
+
this.address = EvmAddressSchema.parse(address);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
setWallet(wallet) {
|
|
55
|
+
if (wallet !== ChainWallet.Metamask && wallet !== ChainWallet.CoinBase) {
|
|
56
|
+
throw new WalletNotFoundException(`wallet ${wallet} not supported on EVM connector`);
|
|
57
|
+
}
|
|
58
|
+
this.wallet = wallet;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
/** Resolve the EIP-1193 provider for the configured wallet. */
|
|
62
|
+
getWalletProvider() {
|
|
63
|
+
if (this.wallet === ChainWallet.Metamask) {
|
|
64
|
+
return selectMetaMaskProvider();
|
|
65
|
+
}
|
|
66
|
+
if (this.wallet === ChainWallet.CoinBase) {
|
|
67
|
+
return selectCoinbaseProvider();
|
|
68
|
+
}
|
|
69
|
+
throw new WalletNotFoundException(`wallet ${this.wallet} not implemented`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Ensure the wallet is connected to the expected address and chain.
|
|
73
|
+
* Unlike v1.0.1 this is iterative (max 3 retries) — there is no
|
|
74
|
+
* recursive self-call that can spin a popup loop.
|
|
75
|
+
*/
|
|
76
|
+
async handleWallet(expectedAddress) {
|
|
77
|
+
const expected = EvmAddressSchema.parse(expectedAddress);
|
|
78
|
+
const provider = this.getWalletProvider();
|
|
79
|
+
const meta = getChainMetadata(this.chain, this.network);
|
|
80
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
81
|
+
let accounts = await getAccounts(provider);
|
|
82
|
+
if (!(await isWalletConnected(provider)) || accounts.length === 0) {
|
|
83
|
+
accounts = await requestAccounts(provider);
|
|
84
|
+
}
|
|
85
|
+
const first = accounts[0];
|
|
86
|
+
if (first === undefined) {
|
|
87
|
+
throw new WalletNotFoundException('wallet returned no accounts');
|
|
88
|
+
}
|
|
89
|
+
if (first.toLowerCase() !== expected) {
|
|
90
|
+
// Ask for permissions once; do NOT recurse forever.
|
|
91
|
+
await provider.request({
|
|
92
|
+
method: 'wallet_requestPermissions',
|
|
93
|
+
params: [{ eth_accounts: {} }],
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const currentChainId = await getChainId(provider);
|
|
98
|
+
if (currentChainId.toLowerCase() !== meta.chainIdHex.toLowerCase()) {
|
|
99
|
+
try {
|
|
100
|
+
await provider.request({
|
|
101
|
+
method: 'wallet_switchEthereumChain',
|
|
102
|
+
params: [{ chainId: meta.chainIdHex }],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
await provider.request({
|
|
107
|
+
method: 'wallet_addEthereumChain',
|
|
108
|
+
params: [
|
|
109
|
+
{
|
|
110
|
+
chainId: meta.chainIdHex,
|
|
111
|
+
chainName: meta.chainName,
|
|
112
|
+
nativeCurrency: meta.nativeCurrency,
|
|
113
|
+
rpcUrls: meta.rpcUrls,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
throw new AccountChangedException(`wallet did not converge to ${expected} on ${this.chain}/${this.network}`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Connect + EIP-712 typed login. Returns the signature; the caller is
|
|
126
|
+
* expected to submit it to the droplinked auth API.
|
|
127
|
+
*/
|
|
128
|
+
async walletLogin() {
|
|
129
|
+
const provider = this.getWalletProvider();
|
|
130
|
+
const accountsBefore = await getAccounts(provider);
|
|
131
|
+
const accounts = accountsBefore.length > 0 ? accountsBefore : await requestAccounts(provider);
|
|
132
|
+
const first = accounts[0];
|
|
133
|
+
if (first === undefined) {
|
|
134
|
+
throw new WalletNotFoundException('wallet returned no accounts');
|
|
135
|
+
}
|
|
136
|
+
const address = EvmAddressSchema.parse(first);
|
|
137
|
+
const meta = getChainMetadata(this.chain, this.network);
|
|
138
|
+
const currentChainId = await getChainId(provider);
|
|
139
|
+
if (currentChainId.toLowerCase() !== meta.chainIdHex.toLowerCase()) {
|
|
140
|
+
throw new ChainMismatchError(`wallet on chain ${currentChainId} but ${meta.chainIdHex} expected`);
|
|
141
|
+
}
|
|
142
|
+
const payload = buildLoginPayload({
|
|
143
|
+
address,
|
|
144
|
+
chainId: meta.chainIdNumber,
|
|
145
|
+
origin: this.origin,
|
|
146
|
+
expiresInSeconds: 60 * 10, // 10 minutes
|
|
147
|
+
});
|
|
148
|
+
const signature = await signLoginPayload(provider, payload);
|
|
149
|
+
this.address = address;
|
|
150
|
+
return { address, signature };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* ERC-20 token transfer.
|
|
154
|
+
*
|
|
155
|
+
* NOTE: This issues a direct `transfer` call. We intentionally do *not*
|
|
156
|
+
* issue an `approve` against an unbounded spender — the drainer-style
|
|
157
|
+
* "approve max uint256 to a router" pattern is rejected at the API
|
|
158
|
+
* boundary. Callers wanting allowance flows must use a different
|
|
159
|
+
* specialized package.
|
|
160
|
+
*/
|
|
161
|
+
async paymentWithToken(receiver, amount, tokenAddress) {
|
|
162
|
+
const recv = EvmAddressSchema.parse(receiver);
|
|
163
|
+
const token = EvmAddressSchema.parse(tokenAddress);
|
|
164
|
+
if (typeof amount !== 'bigint' || amount <= 0n) {
|
|
165
|
+
throw new TypeError('amount must be a positive bigint');
|
|
166
|
+
}
|
|
167
|
+
await this.handleWallet(this.address);
|
|
168
|
+
const provider = this.getWalletProvider();
|
|
169
|
+
const data = encodeErc20Transfer(recv, amount);
|
|
170
|
+
const txHash = await provider.request({
|
|
171
|
+
method: 'eth_sendTransaction',
|
|
172
|
+
params: [
|
|
173
|
+
{
|
|
174
|
+
from: this.address,
|
|
175
|
+
to: token,
|
|
176
|
+
data,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
if (typeof txHash !== 'string' || !/^0x[a-fA-F0-9]{64}$/u.test(txHash)) {
|
|
181
|
+
throw new Error('wallet returned malformed tx hash');
|
|
182
|
+
}
|
|
183
|
+
return txHash;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Droplinked checkout contract call. Requires
|
|
187
|
+
* `checkoutContractAddress` to be supplied at construction time —
|
|
188
|
+
* we will never fetch it from a remote endpoint.
|
|
189
|
+
*/
|
|
190
|
+
async payment(data) {
|
|
191
|
+
const parsed = ChainPaymentSchema.parse(data);
|
|
192
|
+
if (this.checkoutContractAddress === undefined) {
|
|
193
|
+
throw new Error('payment() requires checkoutContractAddress to be set; remote address discovery has been removed');
|
|
194
|
+
}
|
|
195
|
+
await this.handleWallet(this.address);
|
|
196
|
+
// We do NOT decode/encode the legacy droplinkedPurchase ABI here —
|
|
197
|
+
// that surface is feature-frozen and consumers (Next-ShopFront) drive
|
|
198
|
+
// it via a server-built calldata blob. Surface the lower-level
|
|
199
|
+
// eth_sendTransaction primitive so callers retain control.
|
|
200
|
+
void parsed;
|
|
201
|
+
throw new Error('EVMConnector.payment(): direct ABI encoding intentionally removed. ' +
|
|
202
|
+
'Use submitRawTransaction() with calldata produced by the droplinked checkout API.');
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Submit pre-built calldata produced server-side. The calldata is
|
|
206
|
+
* NOT trusted blindly — we enforce two boundary checks before
|
|
207
|
+
* surfacing the transaction to the wallet:
|
|
208
|
+
*
|
|
209
|
+
* 1. `to` MUST equal the connector's configured
|
|
210
|
+
* `checkoutContractAddress`. Sending arbitrary calldata to an
|
|
211
|
+
* arbitrary destination would resurrect the drainer pathway
|
|
212
|
+
* that motivated removing the v1.0.1 remote-ABI fetch.
|
|
213
|
+
* 2. The first 4 bytes of `data` (the function selector) MUST be
|
|
214
|
+
* on the explicit allowlist below. Notably the selectors for
|
|
215
|
+
* `approve(address,uint256)`, `setApprovalForAll(...)`, NFT
|
|
216
|
+
* `safeTransferFrom(...)`, and ERC-20 `permit(...)` are
|
|
217
|
+
* rejected — these are the canonical "drainer" entry points
|
|
218
|
+
* that a compromised checkout API or consumer-side XSS would
|
|
219
|
+
* try to coerce the user into signing.
|
|
220
|
+
*
|
|
221
|
+
* Both checks throw `InvalidTransactionError` BEFORE any wallet
|
|
222
|
+
* prompt appears, so the user is never shown a malicious payload.
|
|
223
|
+
*/
|
|
224
|
+
async submitRawTransaction(args) {
|
|
225
|
+
if (this.checkoutContractAddress === undefined) {
|
|
226
|
+
throw new InvalidTransactionError('submitRawTransaction() requires checkoutContractAddress to be configured on the connector');
|
|
227
|
+
}
|
|
228
|
+
const to = EvmAddressSchema.parse(args.to);
|
|
229
|
+
if (to !== this.checkoutContractAddress.toLowerCase()) {
|
|
230
|
+
throw new InvalidTransactionError(`submitRawTransaction(): destination ${to} does not match configured checkout contract ${this.checkoutContractAddress}`);
|
|
231
|
+
}
|
|
232
|
+
assertCalldataSelectorAllowed(args.data);
|
|
233
|
+
await this.handleWallet(this.address);
|
|
234
|
+
const provider = this.getWalletProvider();
|
|
235
|
+
const params = {
|
|
236
|
+
from: this.address,
|
|
237
|
+
to: args.to,
|
|
238
|
+
data: args.data,
|
|
239
|
+
value: `0x${args.value.toString(16)}`,
|
|
240
|
+
};
|
|
241
|
+
if (args.gasLimit !== undefined) {
|
|
242
|
+
params['gas'] = `0x${args.gasLimit.toString(16)}`;
|
|
243
|
+
}
|
|
244
|
+
const txHash = await provider.request({
|
|
245
|
+
method: 'eth_sendTransaction',
|
|
246
|
+
params: [params],
|
|
247
|
+
});
|
|
248
|
+
if (typeof txHash !== 'string' || !/^0x[a-fA-F0-9]{64}$/u.test(txHash)) {
|
|
249
|
+
throw new Error('wallet returned malformed tx hash');
|
|
250
|
+
}
|
|
251
|
+
return txHash;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/* -------------------------------------------------------------------------- */
|
|
255
|
+
/* submitRawTransaction calldata-selector allowlist */
|
|
256
|
+
/* -------------------------------------------------------------------------- */
|
|
257
|
+
/**
|
|
258
|
+
* 4-byte function selectors that the droplinked checkout flow is known
|
|
259
|
+
* to call. Anything else is rejected at the boundary.
|
|
260
|
+
*
|
|
261
|
+
* - `0xa9059cbb` — ERC-20 `transfer(address,uint256)`
|
|
262
|
+
* The standard token payment path used by `paymentWithToken`.
|
|
263
|
+
* - `0x6a627842` — droplinked legacy checkout `droplinkedPurchase(...)`
|
|
264
|
+
* Selector matches the calldata blob produced by the droplinked
|
|
265
|
+
* checkout API. Replace if the API rotates to a new function.
|
|
266
|
+
* - `0x` (empty data) — native-currency transfer to the checkout
|
|
267
|
+
* contract. Allowed because there is no selector to spoof; the
|
|
268
|
+
* destination is already pinned by the `to === checkoutContract`
|
|
269
|
+
* check.
|
|
270
|
+
*/
|
|
271
|
+
export const SUBMIT_RAW_TX_SELECTOR_ALLOWLIST = new Set([
|
|
272
|
+
'0xa9059cbb',
|
|
273
|
+
'0x6a627842',
|
|
274
|
+
]);
|
|
275
|
+
/**
|
|
276
|
+
* Drainer selectors callers will sometimes ask the wallet to sign.
|
|
277
|
+
* Listed explicitly so the rejection message names them — when one of
|
|
278
|
+
* these appears in calldata, that is *strong* evidence that the
|
|
279
|
+
* checkout API or the consumer page is compromised.
|
|
280
|
+
*/
|
|
281
|
+
export const KNOWN_DRAINER_SELECTORS = {
|
|
282
|
+
'0x095ea7b3': 'approve(address,uint256)',
|
|
283
|
+
'0xa22cb465': 'setApprovalForAll(address,bool)',
|
|
284
|
+
'0x42842e0e': 'safeTransferFrom(address,address,uint256) (NFT)',
|
|
285
|
+
'0xb88d4fde': 'safeTransferFrom(address,address,uint256,bytes) (NFT)',
|
|
286
|
+
'0x23b872dd': 'transferFrom(address,address,uint256)',
|
|
287
|
+
'0xd505accf': 'permit(address,address,uint256,uint256,uint8,bytes32,bytes32)',
|
|
288
|
+
};
|
|
289
|
+
function assertCalldataSelectorAllowed(data) {
|
|
290
|
+
if (typeof data !== 'string' || !/^0x[a-fA-F0-9]*$/u.test(data)) {
|
|
291
|
+
throw new InvalidTransactionError('submitRawTransaction(): data must be a 0x-prefixed hex string');
|
|
292
|
+
}
|
|
293
|
+
// Native-currency transfer (empty data) is allowed — destination is
|
|
294
|
+
// already pinned to the checkout contract.
|
|
295
|
+
if (data === '0x' || data.length === 2) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (data.length < 10) {
|
|
299
|
+
throw new InvalidTransactionError(`submitRawTransaction(): calldata too short to contain a 4-byte selector (got ${data.length - 2} hex chars)`);
|
|
300
|
+
}
|
|
301
|
+
const selector = data.slice(0, 10).toLowerCase();
|
|
302
|
+
const drainer = KNOWN_DRAINER_SELECTORS[selector];
|
|
303
|
+
if (drainer !== undefined) {
|
|
304
|
+
throw new InvalidTransactionError(`submitRawTransaction(): refused drainer-style selector ${selector} (${drainer})`);
|
|
305
|
+
}
|
|
306
|
+
if (!SUBMIT_RAW_TX_SELECTOR_ALLOWLIST.has(selector)) {
|
|
307
|
+
throw new InvalidTransactionError(`submitRawTransaction(): selector ${selector} is not on the allowlist`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function resolveOrigin() {
|
|
311
|
+
const g = globalThis;
|
|
312
|
+
return g.location?.origin ?? 'https://droplinked.com';
|
|
313
|
+
}
|
|
314
|
+
/* -------------------------------------------------------------------------- */
|
|
315
|
+
/* Internal: hand-written ERC-20 transfer encoder */
|
|
316
|
+
/* -------------------------------------------------------------------------- */
|
|
317
|
+
/** Returns the calldata blob for `transfer(address,uint256)`. */
|
|
318
|
+
export function encodeErc20Transfer(to, amount) {
|
|
319
|
+
if (typeof amount !== 'bigint' || amount < 0n) {
|
|
320
|
+
throw new TypeError('amount must be a non-negative bigint');
|
|
321
|
+
}
|
|
322
|
+
// keccak256('transfer(address,uint256)').slice(0,4) = 0xa9059cbb
|
|
323
|
+
const selector = 'a9059cbb';
|
|
324
|
+
const addressPart = to.toLowerCase().replace(/^0x/u, '').padStart(64, '0');
|
|
325
|
+
const amountPart = amount.toString(16).padStart(64, '0');
|
|
326
|
+
return `0x${selector}${addressPart}${amountPart}`;
|
|
327
|
+
}
|
|
328
|
+
// Re-export the ABI fragment for callers that want to introspect.
|
|
329
|
+
export { ERC20_TRANSFER_ABI };
|
|
330
|
+
//# sourceMappingURL=evm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evm.js","sourceRoot":"","sources":["../../src/connectors/evm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phantom (Solana) connector.
|
|
3
|
+
*
|
|
4
|
+
* Hardening deltas vs. v1.0.1:
|
|
5
|
+
* 1. No `window.open('https://phantom.app/', '_blank')` redirect — the
|
|
6
|
+
* original silently popped a new tab when Phantom was missing, which
|
|
7
|
+
* is a phishing-friendly UX. We throw a typed error and let the host
|
|
8
|
+
* app decide what to render.
|
|
9
|
+
* 2. No `while(true) { sleep; getParsedTransaction }` busy-loop — the
|
|
10
|
+
* original could hang a tab forever if the network stalled. We
|
|
11
|
+
* surface the unsigned-transaction hash and let the caller poll.
|
|
12
|
+
* 3. No `console.log(error); throw error` — errors propagate without
|
|
13
|
+
* leaking through stdout.
|
|
14
|
+
* 4. Signature buffer is returned as base64 rather than the raw
|
|
15
|
+
* Uint8Array — same format as MetaMask EIP-712 output so server
|
|
16
|
+
* verification is uniform.
|
|
17
|
+
* 5. Login signs a SIWS-style (Sign-In With Solana, https://siws.xyz/)
|
|
18
|
+
* JSON payload with domain, nonce, chainId, issuedAt,
|
|
19
|
+
* expirationTime — NOT a static plaintext. The signature is
|
|
20
|
+
* bound to origin + chain + time and cannot be replayed across
|
|
21
|
+
* sites or sessions. Verification is provided alongside.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { Chain, ChainWallet, Network, type ChainProvider, type IChainPayment } from '../types.js';
|
|
25
|
+
/**
|
|
26
|
+
* Default human-readable statement embedded in the signed payload so
|
|
27
|
+
* Phantom's prompt explains *why* the user is signing.
|
|
28
|
+
*/
|
|
29
|
+
export declare const DEFAULT_SOLANA_LOGIN_STATEMENT = "Sign in to droplinked. This signature does not authorize any token transfer.";
|
|
30
|
+
/** Allowed Solana chain identifiers; `chain` is bound into the payload. */
|
|
31
|
+
export declare const SOLANA_CHAIN_IDS: readonly ["solana-mainnet", "solana-devnet", "solana-testnet"];
|
|
32
|
+
export type SolanaChainId = (typeof SOLANA_CHAIN_IDS)[number];
|
|
33
|
+
/** Hard upper bound on payload TTL — refused at build time. */
|
|
34
|
+
export declare const SOLANA_LOGIN_MAX_TTL_SECONDS: number;
|
|
35
|
+
/** Default TTL when caller does not specify one. */
|
|
36
|
+
export declare const SOLANA_LOGIN_DEFAULT_TTL_SECONDS: number;
|
|
37
|
+
export declare const SolanaLoginPayloadSchema: z.ZodObject<{
|
|
38
|
+
domain: z.ZodString;
|
|
39
|
+
address: z.ZodString;
|
|
40
|
+
chain: z.ZodEnum<["solana-mainnet", "solana-devnet", "solana-testnet"]>;
|
|
41
|
+
nonce: z.ZodString;
|
|
42
|
+
issuedAt: z.ZodString;
|
|
43
|
+
expirationTime: z.ZodString;
|
|
44
|
+
statement: z.ZodString;
|
|
45
|
+
}, "strip", z.ZodTypeAny, {
|
|
46
|
+
domain: string;
|
|
47
|
+
address: string;
|
|
48
|
+
statement: string;
|
|
49
|
+
nonce: string;
|
|
50
|
+
issuedAt: string;
|
|
51
|
+
expirationTime: string;
|
|
52
|
+
chain: "solana-mainnet" | "solana-devnet" | "solana-testnet";
|
|
53
|
+
}, {
|
|
54
|
+
domain: string;
|
|
55
|
+
address: string;
|
|
56
|
+
statement: string;
|
|
57
|
+
nonce: string;
|
|
58
|
+
issuedAt: string;
|
|
59
|
+
expirationTime: string;
|
|
60
|
+
chain: "solana-mainnet" | "solana-devnet" | "solana-testnet";
|
|
61
|
+
}>;
|
|
62
|
+
export type SolanaLoginPayload = z.infer<typeof SolanaLoginPayloadSchema>;
|
|
63
|
+
export interface BuildSolanaLoginPayloadArgs {
|
|
64
|
+
readonly address: string;
|
|
65
|
+
readonly chain: SolanaChainId;
|
|
66
|
+
readonly origin: string;
|
|
67
|
+
readonly statement?: string;
|
|
68
|
+
readonly expiresInSeconds?: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a SIWS-style login payload binding the signature to
|
|
72
|
+
* (domain, chain, nonce, issuedAt, expirationTime). Refuses any TTL
|
|
73
|
+
* over `SOLANA_LOGIN_MAX_TTL_SECONDS`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildSolanaLoginPayload(args: BuildSolanaLoginPayloadArgs): SolanaLoginPayload;
|
|
76
|
+
/**
|
|
77
|
+
* Canonicalize the payload to a deterministic JSON string with sorted
|
|
78
|
+
* keys. This is what gets signed — same string on both sides of the
|
|
79
|
+
* verification boundary.
|
|
80
|
+
*/
|
|
81
|
+
export declare function canonicalizeSolanaLoginPayload(payload: SolanaLoginPayload): string;
|
|
82
|
+
/** Decode a base58-encoded string into bytes. Throws on invalid input. */
|
|
83
|
+
export declare function base58Decode(input: string): Uint8Array;
|
|
84
|
+
export interface VerifySolanaLoginArgs {
|
|
85
|
+
readonly payload: SolanaLoginPayload;
|
|
86
|
+
/** base64-encoded ed25519 signature returned by `walletLogin()`. */
|
|
87
|
+
readonly signature: string;
|
|
88
|
+
readonly expectedAddress: string;
|
|
89
|
+
readonly expectedChain: SolanaChainId;
|
|
90
|
+
readonly expectedOrigin: string;
|
|
91
|
+
readonly nowMs?: number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Verify a Phantom (Solana) SIWS-style login signature.
|
|
95
|
+
*
|
|
96
|
+
* Checks (in order):
|
|
97
|
+
* 1. Payload shape validates against `SolanaLoginPayloadSchema`.
|
|
98
|
+
* 2. `payload.chain === expectedChain` (cross-network replay refused).
|
|
99
|
+
* 3. `payload.domain === host(expectedOrigin)` (origin-spoofing refused).
|
|
100
|
+
* 4. `payload.address === expectedAddress`.
|
|
101
|
+
* 5. `now < expirationTime` AND `issuedAt <= now` (freshness).
|
|
102
|
+
* 6. The base64 signature is a valid ed25519 signature over
|
|
103
|
+
* `utf8(canonicalizeSolanaLoginPayload(payload))` for the public
|
|
104
|
+
* key derived from `expectedAddress` (base58-decoded).
|
|
105
|
+
*
|
|
106
|
+
* Note: nonce replay tracking is the responsibility of the server-side
|
|
107
|
+
* caller — this function does not maintain a seen-nonces store. The
|
|
108
|
+
* canonical pattern is to record `(payload.nonce, payload.address)`
|
|
109
|
+
* in a short-lived store keyed by `expirationTime` and reject any
|
|
110
|
+
* second presentation.
|
|
111
|
+
*/
|
|
112
|
+
export declare function verifySolanaLoginSignature(args: VerifySolanaLoginArgs): Promise<void>;
|
|
113
|
+
export interface PhantomConnectorOptions {
|
|
114
|
+
readonly network: Network;
|
|
115
|
+
/** Origin to bind into the login payload. Defaults to globalThis.location.origin. */
|
|
116
|
+
readonly origin?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Solana chain identifier to bind into the login payload. Defaults
|
|
119
|
+
* to `solana-mainnet` on Network.MAINNET and `solana-devnet` on
|
|
120
|
+
* Network.TESTNET.
|
|
121
|
+
*/
|
|
122
|
+
readonly chainId?: SolanaChainId;
|
|
123
|
+
}
|
|
124
|
+
export declare class PhantomConnector implements ChainProvider {
|
|
125
|
+
readonly chain: Chain;
|
|
126
|
+
readonly network: Network;
|
|
127
|
+
address: string;
|
|
128
|
+
wallet: ChainWallet;
|
|
129
|
+
private readonly origin;
|
|
130
|
+
private readonly chainId;
|
|
131
|
+
constructor(networkOrOptions: Network | PhantomConnectorOptions);
|
|
132
|
+
setAddress(address: string): this;
|
|
133
|
+
setWallet(wallet: ChainWallet): this;
|
|
134
|
+
/**
|
|
135
|
+
* Connect + SIWS-style typed login. Builds a fresh payload with a
|
|
136
|
+
* 256-bit nonce, binds it to origin + chain + time, asks Phantom to
|
|
137
|
+
* sign the canonicalized JSON, and returns the payload alongside the
|
|
138
|
+
* base64 signature. Server-side verification uses
|
|
139
|
+
* `verifySolanaLoginSignature`.
|
|
140
|
+
*
|
|
141
|
+
* Replaces the v1.0.1 / pre-audit static-plaintext signMessage path,
|
|
142
|
+
* which was replayable across origins, chains, and sessions.
|
|
143
|
+
*/
|
|
144
|
+
walletLogin(): Promise<{
|
|
145
|
+
address: string;
|
|
146
|
+
signature: string;
|
|
147
|
+
payload: SolanaLoginPayload;
|
|
148
|
+
}>;
|
|
149
|
+
/**
|
|
150
|
+
* SPL token transfer is intentionally not implemented in this minimal
|
|
151
|
+
* recover+harden. The original v1.0.1 relied on the long-deprecated
|
|
152
|
+
* `@solana/spl-token` v0.1.x Token-class API (removed in v0.2). Re-
|
|
153
|
+
* implementing it correctly against the current `@solana/spl-token` v0.4
|
|
154
|
+
* APIs is out of scope for the first PR; it will land in a follow-up.
|
|
155
|
+
*
|
|
156
|
+
* The method throws a typed error so consumers fail loudly rather than
|
|
157
|
+
* silently mis-routing a transfer.
|
|
158
|
+
*/
|
|
159
|
+
paymentWithToken(_receiver: string, _amount: bigint, _tokenAddress: string): Promise<string>;
|
|
160
|
+
payment(_data: IChainPayment): Promise<{
|
|
161
|
+
deploy_hash: string;
|
|
162
|
+
cryptoAmount: bigint;
|
|
163
|
+
}>;
|
|
164
|
+
}
|
|
165
|
+
/** base64-encode a Uint8Array. Browser-safe; no Buffer dependency. */
|
|
166
|
+
export declare function toBase64(bytes: Uint8Array): string;
|
|
167
|
+
//# sourceMappingURL=phantom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phantom.d.ts","sourceRoot":"","sources":["../../src/connectors/phantom.ts"],"names":[],"mappings":"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"}
|