186 lines
5.0 KiB
TypeScript
186 lines
5.0 KiB
TypeScript
import {
|
|
Client,
|
|
Wallet,
|
|
xrpToDrops,
|
|
dropsToXrp,
|
|
isValidClassicAddress,
|
|
} from 'xrpl'
|
|
import type {
|
|
XrpBalance,
|
|
XrpTxDetails,
|
|
XrpTxEntry,
|
|
XrpWalletInfo,
|
|
} from '../../../lib/rpc-schema'
|
|
import { getSecret, setSecret } from '../secrets-cache'
|
|
|
|
const SERVER = 'wss://s1.ripple.com'
|
|
const RIPPLE_EPOCH = 946684800
|
|
const WALLET_LIST_KEY = 'xrp_wallets'
|
|
const SERVICE = 'koins'
|
|
|
|
let client: Client | null = null
|
|
|
|
export async function getClient(): Promise<Client> {
|
|
if (client?.isConnected()) return client
|
|
client = new Client(SERVER)
|
|
await client.connect()
|
|
return client
|
|
}
|
|
|
|
export function isValidAddress(address: string): boolean {
|
|
return isValidClassicAddress(address)
|
|
}
|
|
|
|
export function generateWallet(): { address: string; seed: string } {
|
|
const wallet = Wallet.generate()
|
|
return { address: wallet.classicAddress, seed: wallet.seed! }
|
|
}
|
|
|
|
export function walletFromSecret(secret: string): { address: string } {
|
|
const s = secret.trim()
|
|
try {
|
|
return { address: Wallet.fromSeed(s).classicAddress }
|
|
} catch {
|
|
return { address: Wallet.fromMnemonic(s).classicAddress }
|
|
}
|
|
}
|
|
|
|
export function walletForSigning(secret: string): Wallet {
|
|
const s = secret.trim()
|
|
try {
|
|
return Wallet.fromSeed(s)
|
|
} catch {
|
|
return Wallet.fromMnemonic(s)
|
|
}
|
|
}
|
|
|
|
export async function getXrpWalletList(): Promise<XrpWalletInfo[]> {
|
|
const raw = await getSecret(SERVICE, WALLET_LIST_KEY)
|
|
if (!raw) return []
|
|
try {
|
|
return JSON.parse(raw)
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export async function saveXrpWalletList(
|
|
wallets: XrpWalletInfo[],
|
|
): Promise<void> {
|
|
await setSecret(SERVICE, WALLET_LIST_KEY, JSON.stringify(wallets))
|
|
}
|
|
|
|
export async function getBalance(address: string): Promise<XrpBalance> {
|
|
const c = await getClient()
|
|
try {
|
|
const balance = await c.getXrpBalance(address)
|
|
return { address, balance: String(balance), funded: true }
|
|
} catch (e) {
|
|
if (e instanceof Error && e.message.includes('Account not found')) {
|
|
return { address, balance: '0', funded: false }
|
|
}
|
|
throw e
|
|
}
|
|
}
|
|
|
|
function dropsAmount(value: unknown): string {
|
|
if (typeof value !== 'string') return '0'
|
|
return String(dropsToXrp(value))
|
|
}
|
|
|
|
export async function getTransactions(address: string): Promise<XrpTxEntry[]> {
|
|
const c = await getClient()
|
|
const res = await c.request({
|
|
command: 'account_tx',
|
|
account: address,
|
|
ledger_index_min: -1,
|
|
ledger_index_max: -1,
|
|
limit: 25,
|
|
})
|
|
const entries: XrpTxEntry[] = []
|
|
for (const entry of res.result.transactions as any[]) {
|
|
const tx = entry.tx_json ?? entry.tx
|
|
if (!tx || tx.TransactionType !== 'Payment') continue
|
|
const meta = entry.meta ?? entry.metaData ?? {}
|
|
if (meta.TransactionResult && meta.TransactionResult !== 'tesSUCCESS')
|
|
continue
|
|
const delivered = meta.delivered_amount ?? meta.DeliveredAmount ?? tx.Amount
|
|
entries.push({
|
|
hash: entry.hash ?? tx.hash,
|
|
amount: dropsAmount(delivered),
|
|
timestamp: tx.date ? String(tx.date + RIPPLE_EPOCH) : '0',
|
|
direction: tx.Destination === address ? 'in' : 'out',
|
|
from: tx.Account,
|
|
to: tx.Destination,
|
|
destinationTag: tx.DestinationTag,
|
|
fee: dropsAmount(tx.Fee),
|
|
confirmed: entry.validated === true,
|
|
})
|
|
}
|
|
return entries
|
|
}
|
|
|
|
export async function getTxDetails(
|
|
hash: string,
|
|
): Promise<XrpTxDetails | null> {
|
|
const c = await getClient()
|
|
try {
|
|
const res = await c.request({ command: 'tx', transaction: hash })
|
|
const d = res.result as any
|
|
const tx = d.tx_json ?? d
|
|
if (tx.TransactionType !== 'Payment') return null
|
|
const meta = d.meta ?? {}
|
|
const delivered = meta.delivered_amount ?? meta.DeliveredAmount ?? tx.Amount
|
|
return {
|
|
hash: d.hash ?? hash,
|
|
from: tx.Account,
|
|
to: tx.Destination,
|
|
amount: dropsAmount(delivered),
|
|
fee: dropsAmount(tx.Fee),
|
|
timestamp: tx.date ? String(tx.date + RIPPLE_EPOCH) : '0',
|
|
ledgerIndex: d.ledger_index ?? 0,
|
|
destinationTag: tx.DestinationTag,
|
|
confirmed: d.validated === true,
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function sendPayment(
|
|
secret: string,
|
|
to: string,
|
|
amount: string,
|
|
destinationTag?: number,
|
|
): Promise<{ hash: string; fee: string }> {
|
|
if (!isValidClassicAddress(to)) throw new Error('Invalid XRP address')
|
|
const wallet = walletForSigning(secret)
|
|
const c = await getClient()
|
|
const prepared = await c.autofill({
|
|
TransactionType: 'Payment',
|
|
Account: wallet.classicAddress,
|
|
Amount: xrpToDrops(amount),
|
|
Destination: to,
|
|
...(destinationTag !== undefined ? { DestinationTag: destinationTag } : {}),
|
|
})
|
|
const signed = wallet.sign(prepared)
|
|
const result = await c.submit(signed.tx_blob)
|
|
const engineResult = (result.result as any).engine_result
|
|
if (engineResult !== 'tesSUCCESS') {
|
|
const msg =
|
|
(result.result as any).engine_result_message ?? 'Transaction failed'
|
|
throw new Error(`${engineResult}: ${msg}`)
|
|
}
|
|
return { hash: signed.hash, fee: String(dropsToXrp((prepared as any).Fee ?? '0')) }
|
|
}
|
|
|
|
export async function getFee(): Promise<{ fee: string } | null> {
|
|
try {
|
|
const c = await getClient()
|
|
const res = await c.request({ command: 'fee' })
|
|
return { fee: String(dropsToXrp(res.result.drops.open_ledger_fee)) }
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|