@chainberry/berry-signer 1.0.0
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/README.md +117 -0
- package/package.json +45 -0
- package/src/btc/btc-sign.ts +22 -0
- package/src/evm/evm-sign.ts +43 -0
- package/src/index.ts +7 -0
- package/src/ltc/ltc-sign.ts +53 -0
- package/src/sol/sol-sign.ts +31 -0
- package/src/ton/ton-sign.ts +38 -0
- package/src/trx/trx-sign.ts +17 -0
- package/src/utxo/parse-private-key.ts +14 -0
- package/src/xrp/xrp-sign.ts +21 -0
- package/tsconfig.json +11 -0
- package/tsconfig.lib.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# BerrySigner
|
|
2
|
+
|
|
3
|
+
Pure signing library for multiple blockchain networks. No network calls — takes an unsigned transaction prepared elsewhere and signs it locally with a private key. Designed to run on the frontend (mobile wallet, browser extension) while the unsigned transaction is prepared on the backend.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install berry-signer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Supported Chains
|
|
12
|
+
|
|
13
|
+
| Chain | Function |
|
|
14
|
+
|-------|----------|
|
|
15
|
+
| EVM (ETH, BNB, POL, AVAX, ARB, OP, BASE, SONIC) | `signEvmTransaction` |
|
|
16
|
+
| Bitcoin | `signBtcTransaction` |
|
|
17
|
+
| Litecoin | `signLtcTransaction` |
|
|
18
|
+
| Solana | `signSolTransaction` |
|
|
19
|
+
| TRON | `signTrxTransaction` |
|
|
20
|
+
| XRP Ledger | `signXrpTransaction` |
|
|
21
|
+
| TON | `signTonTransaction` |
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Each function takes a private key and an unsigned transaction prepared by the backend, and returns a signed transaction ready for broadcast.
|
|
26
|
+
|
|
27
|
+
### EVM (ETH, BNB, POL, AVAX, ARB, OP, BASE, SONIC)
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { signEvmTransaction } from "berry-signer";
|
|
31
|
+
|
|
32
|
+
const signedTx = await signEvmTransaction(privateKeyHex, unsignedTx);
|
|
33
|
+
// signedTx is a hex string ready to broadcast via eth_sendRawTransaction
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Bitcoin
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { signBtcTransaction } from "berry-signer";
|
|
40
|
+
|
|
41
|
+
const signedTxHex = signBtcTransaction(privateKeyHex, unsignedPsbtBase64, isMainnet);
|
|
42
|
+
// signedTxHex is a raw transaction hex ready for broadcast
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Litecoin
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { signLtcTransaction } from "berry-signer";
|
|
49
|
+
|
|
50
|
+
const signedTxHex = signLtcTransaction(privateKeyHex, unsignedPsbtBase64, isMainnet);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Solana
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { signSolTransaction } from "berry-signer";
|
|
57
|
+
|
|
58
|
+
const signedTxBase64 = signSolTransaction(privateKey, unsignedTxBase64);
|
|
59
|
+
// signedTxBase64 is a base64-encoded signed transaction
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### TRON
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { signTrxTransaction } from "berry-signer";
|
|
66
|
+
|
|
67
|
+
const signedTxJson = await signTrxTransaction(privateKey, unsignedTx, rpcUrl);
|
|
68
|
+
// signedTxJson is a JSON string of the signed transaction
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### XRP Ledger
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { signXrpTransaction } from "berry-signer";
|
|
75
|
+
|
|
76
|
+
const signedTxBlob = signXrpTransaction(privateKeyHex, preparedTx);
|
|
77
|
+
// signedTxBlob is a hex-encoded signed transaction blob
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### TON
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { signTonTransaction } from "berry-signer";
|
|
84
|
+
|
|
85
|
+
const { bocBase64, txHash } = signTonTransaction(privateKeyHex, {
|
|
86
|
+
toAddress,
|
|
87
|
+
amount, // in TON (e.g. "1.5")
|
|
88
|
+
seqno, // wallet seqno fetched from the network
|
|
89
|
+
memoId, // optional memo
|
|
90
|
+
});
|
|
91
|
+
// bocBase64 is the signed BOC ready for broadcast
|
|
92
|
+
// txHash is the pre-computed transaction hash (needed for TON broadcast fallback)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Design
|
|
96
|
+
|
|
97
|
+
The split between signing and broadcasting allows the private key to stay on the client:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Backend Frontend (BerrySigner) Backend
|
|
101
|
+
│ │ │
|
|
102
|
+
│── prepare unsigned tx ──────────────>│ │
|
|
103
|
+
│ │── sign with key │
|
|
104
|
+
│<─────────────────────── signed tx ───│ │
|
|
105
|
+
│ │
|
|
106
|
+
│── broadcast to network ──────────────────────────────────────>│
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Private Key Format
|
|
110
|
+
|
|
111
|
+
All functions accept the private key as a hex string (with or without `0x` prefix), except:
|
|
112
|
+
- **Solana**: accepts hex or base58 encoded private key
|
|
113
|
+
- **TRON**: accepts hex string
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chainberry/berry-signer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Pure signing library for multiple blockchain networks — EVM, BTC, LTC, SOL, TRX, XRP, TON",
|
|
5
|
+
"keywords": ["blockchain", "signing", "wallet", "ethereum", "bitcoin", "solana", "tron", "ton", "xrp", "litecoin"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"bitcoinjs-lib": ">=6.0.0",
|
|
20
|
+
"ecpair": ">=2.0.0",
|
|
21
|
+
"tiny-secp256k1": ">=2.0.0",
|
|
22
|
+
"ethers": ">=6.0.0",
|
|
23
|
+
"@solana/web3.js": ">=1.0.0",
|
|
24
|
+
"tronweb": ">=5.0.0",
|
|
25
|
+
"xrpl": ">=2.0.0",
|
|
26
|
+
"@ton/core": ">=0.50.0",
|
|
27
|
+
"@ton/crypto": ">=3.0.0",
|
|
28
|
+
"@ton/ton": ">=13.0.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"bitcoinjs-lib": { "optional": true },
|
|
32
|
+
"ecpair": { "optional": true },
|
|
33
|
+
"tiny-secp256k1": { "optional": true },
|
|
34
|
+
"ethers": { "optional": true },
|
|
35
|
+
"@solana/web3.js": { "optional": true },
|
|
36
|
+
"tronweb": { "optional": true },
|
|
37
|
+
"xrpl": { "optional": true },
|
|
38
|
+
"@ton/core": { "optional": true },
|
|
39
|
+
"@ton/crypto": { "optional": true },
|
|
40
|
+
"@ton/ton": { "optional": true }
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"tslib": "2.8.1"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as bitcoin from "bitcoinjs-lib";
|
|
2
|
+
import ECPairFactory from "ecpair";
|
|
3
|
+
import * as ecc from "tiny-secp256k1";
|
|
4
|
+
import { parsePrivateKey } from "../utxo/parse-private-key";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pure signing — no network calls. `unsignedPsbtBase64` comes from
|
|
8
|
+
* wallet-broadcast's prepareBtcTransaction (a read-only call that fetches
|
|
9
|
+
* UTXOs/fee rate). Signs every input, finalizes, and returns the raw tx hex
|
|
10
|
+
* ready for broadcast.
|
|
11
|
+
*/
|
|
12
|
+
export function signBtcTransaction(privateKeyHex: string, unsignedPsbtBase64: string, isMainnet: boolean): string {
|
|
13
|
+
const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet;
|
|
14
|
+
const ECPair = ECPairFactory(ecc);
|
|
15
|
+
const keyPair = ECPair.fromPrivateKey(parsePrivateKey(privateKeyHex), { network });
|
|
16
|
+
|
|
17
|
+
const psbt = bitcoin.Psbt.fromBase64(unsignedPsbtBase64, { network });
|
|
18
|
+
for (let i = 0; i < psbt.inputCount; i++) psbt.signInput(i, keyPair);
|
|
19
|
+
psbt.finalizeAllInputs();
|
|
20
|
+
|
|
21
|
+
return psbt.extractTransaction().toHex();
|
|
22
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
|
|
3
|
+
export type EvmUnsignedTx = {
|
|
4
|
+
to: string;
|
|
5
|
+
chainId: number;
|
|
6
|
+
nonce: number;
|
|
7
|
+
gasLimit: string;
|
|
8
|
+
value?: string;
|
|
9
|
+
data?: string;
|
|
10
|
+
gasPrice?: string;
|
|
11
|
+
maxFeePerGas?: string;
|
|
12
|
+
maxPriorityFeePerGas?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function formatPrivateKey(key: string): string {
|
|
16
|
+
return key.startsWith("0x") ? key : `0x${key}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pure signing — no network calls, no key storage/decryption.
|
|
21
|
+
* Caller (frontend or backend) supplies the raw private key directly.
|
|
22
|
+
* `unsignedTx` must already carry nonce/gas/chainId, since those require
|
|
23
|
+
* a prior read-only RPC call (see wallet-broadcast's prepare helpers).
|
|
24
|
+
*/
|
|
25
|
+
export function signEvmTransaction(privateKeyHex: string, unsignedTx: EvmUnsignedTx): Promise<string> {
|
|
26
|
+
const wallet = new ethers.Wallet(formatPrivateKey(privateKeyHex));
|
|
27
|
+
const txRequest: ethers.TransactionRequest = {
|
|
28
|
+
to: unsignedTx.to,
|
|
29
|
+
chainId: unsignedTx.chainId,
|
|
30
|
+
nonce: unsignedTx.nonce,
|
|
31
|
+
gasLimit: BigInt(unsignedTx.gasLimit),
|
|
32
|
+
value: unsignedTx.value !== undefined ? BigInt(unsignedTx.value) : undefined,
|
|
33
|
+
data: unsignedTx.data,
|
|
34
|
+
};
|
|
35
|
+
if (unsignedTx.maxFeePerGas !== undefined) {
|
|
36
|
+
txRequest.maxFeePerGas = BigInt(unsignedTx.maxFeePerGas);
|
|
37
|
+
txRequest.maxPriorityFeePerGas =
|
|
38
|
+
unsignedTx.maxPriorityFeePerGas !== undefined ? BigInt(unsignedTx.maxPriorityFeePerGas) : undefined;
|
|
39
|
+
} else if (unsignedTx.gasPrice !== undefined) {
|
|
40
|
+
txRequest.gasPrice = BigInt(unsignedTx.gasPrice);
|
|
41
|
+
}
|
|
42
|
+
return wallet.signTransaction(txRequest);
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as bitcoin from "bitcoinjs-lib";
|
|
2
|
+
import ECPairFactory from "ecpair";
|
|
3
|
+
import * as ecc from "tiny-secp256k1";
|
|
4
|
+
import { parsePrivateKey } from "../utxo/parse-private-key";
|
|
5
|
+
|
|
6
|
+
const LITECOIN_MAINNET: bitcoin.Network = {
|
|
7
|
+
messagePrefix: "\x19Litecoin Signed Message:\n",
|
|
8
|
+
bech32: "ltc",
|
|
9
|
+
bip32: { public: 0x019da462, private: 0x019d9cfe },
|
|
10
|
+
pubKeyHash: 0x30,
|
|
11
|
+
scriptHash: 0x32,
|
|
12
|
+
wif: 0xb0,
|
|
13
|
+
};
|
|
14
|
+
const LITECOIN_TESTNET: bitcoin.Network = {
|
|
15
|
+
messagePrefix: "\x19Litecoin Signed Message:\n",
|
|
16
|
+
bech32: "tltc",
|
|
17
|
+
bip32: { public: 0x0436f6e1, private: 0x0436ef7d },
|
|
18
|
+
pubKeyHash: 0x6f,
|
|
19
|
+
scriptHash: 0xc4,
|
|
20
|
+
wif: 0xef,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pure signing — no network calls. `unsignedPsbtBase64` comes from
|
|
25
|
+
* wallet-broadcast's prepareLtcTransaction (a read-only call that fetches
|
|
26
|
+
* UTXOs/fee rate). Signs every input, finalizes, and returns the raw tx hex
|
|
27
|
+
* ready for broadcast.
|
|
28
|
+
*/
|
|
29
|
+
export function signLtcTransaction(privateKeyHex: string, unsignedPsbtBase64: string, isMainnet: boolean): string {
|
|
30
|
+
const network = isMainnet ? LITECOIN_MAINNET : LITECOIN_TESTNET;
|
|
31
|
+
const ECPair = ECPairFactory(ecc);
|
|
32
|
+
const keyPair = ECPair.fromPrivateKey(parsePrivateKey(privateKeyHex), { network });
|
|
33
|
+
|
|
34
|
+
const psbt = bitcoin.Psbt.fromBase64(unsignedPsbtBase64, { network });
|
|
35
|
+
for (let i = 0; i < psbt.inputCount; i++) psbt.signInput(i, keyPair);
|
|
36
|
+
psbt.finalizeAllInputs();
|
|
37
|
+
|
|
38
|
+
return psbt.extractTransaction().toHex();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Derives the P2WPKH address for a raw private key. WalletCore (TrustWallet) has no
|
|
43
|
+
* native Litecoin testnet support, so this is the one place that still needs bitcoinjs-lib
|
|
44
|
+
* for address derivation — kept here instead of duplicated at call sites.
|
|
45
|
+
*/
|
|
46
|
+
export function getLtcAddressFromPrivateKey(privateKeyHex: string, isMainnet: boolean): string {
|
|
47
|
+
const network = isMainnet ? LITECOIN_MAINNET : LITECOIN_TESTNET;
|
|
48
|
+
const ECPair = ECPairFactory(ecc);
|
|
49
|
+
const keyPair = ECPair.fromPrivateKey(parsePrivateKey(privateKeyHex), { network });
|
|
50
|
+
const address = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey, network }).address;
|
|
51
|
+
if (!address) throw new Error("Failed to derive LTC address");
|
|
52
|
+
return address;
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import bs58 from "bs58";
|
|
2
|
+
import { Keypair, Transaction } from "@solana/web3.js";
|
|
3
|
+
|
|
4
|
+
function getSolKeypairFromPrivateKey(privateKey: string): Keypair {
|
|
5
|
+
const cleanedKey = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
|
|
6
|
+
let secretKey: Uint8Array;
|
|
7
|
+
if (/^[0-9a-fA-F]+$/.test(cleanedKey)) {
|
|
8
|
+
secretKey = Uint8Array.from(Buffer.from(cleanedKey, "hex"));
|
|
9
|
+
} else {
|
|
10
|
+
try {
|
|
11
|
+
secretKey = bs58.decode(cleanedKey);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
throw new Error(`Invalid private key encoding: not valid hex or base58 (${(error as Error).message})`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (secretKey.length === 64) return Keypair.fromSecretKey(secretKey);
|
|
17
|
+
if (secretKey.length === 32) return Keypair.fromSeed(secretKey);
|
|
18
|
+
throw new Error(`Invalid private key length: ${secretKey.length} bytes. Expected 32 or 64 bytes.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Pure signing — no network calls. `unsignedTxBase64` must already carry
|
|
23
|
+
* recentBlockhash/feePayer/instructions (see wallet-broadcast's
|
|
24
|
+
* prepareSolTransaction, a read-only RPC call).
|
|
25
|
+
*/
|
|
26
|
+
export function signSolTransaction(privateKey: string, unsignedTxBase64: string): string {
|
|
27
|
+
const keypair = getSolKeypairFromPrivateKey(privateKey);
|
|
28
|
+
const transaction = Transaction.from(Buffer.from(unsignedTxBase64, "base64"));
|
|
29
|
+
transaction.sign(keypair);
|
|
30
|
+
return transaction.serialize().toString("base64");
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { external, storeMessage } from "@ton/core";
|
|
2
|
+
import { keyPairFromSeed } from "@ton/crypto";
|
|
3
|
+
import { Address, beginCell, Cell, internal, toNano, WalletContractV4 } from "@ton/ton";
|
|
4
|
+
|
|
5
|
+
function getKeyPairFromPrivateKey(privateKeyHex: string): { publicKey: Buffer; secretKey: Buffer } {
|
|
6
|
+
const raw = privateKeyHex.replace(/^0x/i, "");
|
|
7
|
+
if (raw.length !== 64) throw new Error(`Invalid TON private key length: ${raw.length} hex chars. Expected 64 (32 bytes).`);
|
|
8
|
+
return keyPairFromSeed(Buffer.from(raw, "hex"));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type SignedTonTx = { bocBase64: string; txHash: string };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pure signing — no network calls. `seqno` comes from wallet-broadcast's
|
|
15
|
+
* fetchTonSeqno (a read-only RPC call).
|
|
16
|
+
*/
|
|
17
|
+
export function signTonTransaction(
|
|
18
|
+
privateKeyHex: string,
|
|
19
|
+
params: { toAddress: string; amount: string; seqno: number; memoId?: string }
|
|
20
|
+
): SignedTonTx {
|
|
21
|
+
const keyPair = getKeyPairFromPrivateKey(privateKeyHex);
|
|
22
|
+
const wallet = WalletContractV4.create({ workchain: 0, publicKey: keyPair.publicKey });
|
|
23
|
+
const walletAddress = wallet.address;
|
|
24
|
+
|
|
25
|
+
const recipientAddress = Address.parse(params.toAddress.trim());
|
|
26
|
+
const amountInNano = toNano(params.amount);
|
|
27
|
+
let body: Cell | undefined;
|
|
28
|
+
if (params.memoId) {
|
|
29
|
+
body = beginCell().storeUint(0, 32).storeStringTail(params.memoId).endCell();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const internalMessage = internal({ to: recipientAddress, value: amountInNano, body, bounce: false });
|
|
33
|
+
const transferBody = wallet.createTransfer({ secretKey: keyPair.secretKey, messages: [internalMessage], seqno: params.seqno });
|
|
34
|
+
const extMessage = external({ to: walletAddress, init: params.seqno === 0 ? wallet.init : undefined, body: transferBody });
|
|
35
|
+
const bocBytes = beginCell().store(storeMessage(extMessage)).endCell().toBoc();
|
|
36
|
+
|
|
37
|
+
return { bocBase64: Buffer.from(bocBytes).toString("base64"), txHash: transferBody.hash().toString("hex") };
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
2
|
+
let TronWeb = require("tronweb");
|
|
3
|
+
if (TronWeb.TronWeb) TronWeb = TronWeb.TronWeb;
|
|
4
|
+
else if (TronWeb.default) TronWeb = TronWeb.default;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pure signing — TronWeb's signing call itself makes no network request, but its
|
|
8
|
+
* constructor still requires node URLs (an SDK quirk, not an actual dependency for
|
|
9
|
+
* signing). `rpcUrl` can be any reachable TRON node; it is never called during sign.
|
|
10
|
+
* `unsignedTx` comes from wallet-broadcast's prepareTrxTransaction (a read-only call).
|
|
11
|
+
*/
|
|
12
|
+
export async function signTrxTransaction(privateKey: string, unsignedTx: unknown, rpcUrl: string): Promise<string> {
|
|
13
|
+
const cleanPrivateKey = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
|
|
14
|
+
const tronWeb = new TronWeb({ fullNode: rpcUrl, solidityNode: rpcUrl, eventServer: rpcUrl, privateKey: cleanPrivateKey });
|
|
15
|
+
const signedTx = await tronWeb.trx.sign(unsignedTx, cleanPrivateKey);
|
|
16
|
+
return JSON.stringify(signedTx);
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decodes a raw UTXO-chain private key from either a 32-byte hex string or the
|
|
3
|
+
* legacy hex-of-UTF8-bytes-of-comma-separated-decimal format used by older wallets.
|
|
4
|
+
*/
|
|
5
|
+
export function parsePrivateKey(decryptedKey: string, coinLabel = "UTXO"): Buffer {
|
|
6
|
+
const raw = Buffer.from(decryptedKey.replace(/^0x/, ""), "hex");
|
|
7
|
+
if (raw.length === 32) return raw;
|
|
8
|
+
const asString = raw.toString("utf8");
|
|
9
|
+
const parts = asString.split(",").map(Number);
|
|
10
|
+
if (parts.length === 32 && parts.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {
|
|
11
|
+
return Buffer.from(parts);
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Invalid ${coinLabel} private key: expected 32 bytes, got ${raw.length}`);
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as ecc from "tiny-secp256k1";
|
|
2
|
+
import { Payment, Wallet as XrplWallet } from "xrpl";
|
|
3
|
+
|
|
4
|
+
function buildXrplWallet(privateKeyHex: string): XrplWallet {
|
|
5
|
+
const raw = privateKeyHex.replace(/^0x/i, "");
|
|
6
|
+
const pubKeyBuf = ecc.pointFromScalar(Buffer.from(raw, "hex"), true);
|
|
7
|
+
if (!pubKeyBuf) throw new Error("Failed to derive XRP public key from private key");
|
|
8
|
+
const pubKeyHex = Buffer.from(pubKeyBuf).toString("hex").toUpperCase();
|
|
9
|
+
const prefixedPriv = raw.startsWith("00") ? raw : `00${raw}`;
|
|
10
|
+
return new XrplWallet(pubKeyHex, prefixedPriv);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pure signing — no network calls. `preparedTx` must already carry
|
|
15
|
+
* Sequence/Fee/LastLedgerSequence (xrpl's autofill, a read-only RPC call —
|
|
16
|
+
* see wallet-broadcast's prepareXrpTransaction).
|
|
17
|
+
*/
|
|
18
|
+
export function signXrpTransaction(privateKeyHex: string, preparedTx: Payment): string {
|
|
19
|
+
const wallet = buildXrplWallet(privateKeyHex);
|
|
20
|
+
return wallet.sign(preparedTx).tx_blob;
|
|
21
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": "../../",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
|
|
8
|
+
"emitDeclarationOnly": false,
|
|
9
|
+
"module": "CommonJS",
|
|
10
|
+
"moduleResolution": "Node10",
|
|
11
|
+
"types": ["node"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts"],
|
|
14
|
+
"references": []
|
|
15
|
+
}
|