init
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
The MIT License (MIT)
|
||||
=====================
|
||||
|
||||
Copyright © `2022` `nitowa`
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the “Software”), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
# Overview
|
||||
|
||||
[](https://drone.nitowa.xyz/npm-packages/xrpio)
|
||||
[](https://www.npmjs.com/package/xrpio)
|
||||
[](https://www.npmjs.com/package/xrpio)
|
||||
[](https://gitea.nitowa.xyz/docs/xrpio/src/branch/master/LICENSE.md)
|
||||
|
||||
xrpio is a library that allows you to write and read arbitrary data in the ripple blockchain.
|
||||
|
||||
# How to install
|
||||
```
|
||||
npm i xrpio
|
||||
```
|
||||
|
||||
# How it works
|
||||
|
||||
Transactions on the ripple blockchain are allowed to carry up to 1kB of arbitrary data via the memo field.
|
||||
We can use this to store data of any size by building a tree of references between these transactions that can then be reassembled by reading them back from the blockchain.
|
||||
In order to generate these transactions xrpio sends payments with the minimum denomination between two wallets controlled by the user.
|
||||
|
||||
xrpio automatically takes care of the logistics behind this technique as well as compression of the data.
|
||||
|
||||
Highly simplified, you can visualize the process like this:
|
||||
|
||||

|
||||
|
||||
In practice each node does of course store significantly more data.
|
||||
|
||||
|
||||
# Caution
|
||||
This library is in an early stage of development and **breaking changes may occur spontaneously and without regard of semantic versioning until the v1.0.0 release**.
|
||||
|
||||
## <span style="color:red"> Operation on the main-net is untested and should not be used in production! If you want to deploy this library with the main-net please download the sources and modify them to your needs.</span>
|
||||
<br />
|
||||
|
||||
# Quickstart
|
||||
```typescript
|
||||
import { xrpIO } from "xrpio"
|
||||
|
||||
const api = new xrpIO("wss://some_ripple_node.net:51233")
|
||||
await api.connect()
|
||||
|
||||
txHash = await api.treeWrite("arbitrary text 123", receiveWallet.address, sendWallet.secret)
|
||||
data = await api.treeRead([txHash])
|
||||
|
||||
console.log(data) //"arbitrary text 123"
|
||||
```
|
||||
|
||||
# Known Bugs
|
||||
- When using large data (>300kB) and public nodes, the writing process may fail due to rate limits. xrpio will attempt to mitigate this, but success is inconsistent.
|
||||
|
||||
# [Full documentation](https://gitea.nitowa.xyz/docs/xrpio)
|
||||
+105952
File diff suppressed because one or more lines are too long
+3
@@ -0,0 +1,3 @@
|
||||
export * from './util/types';
|
||||
export * from './util/protocol.constants';
|
||||
export * from './xrpIO/xrpl-binding';
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//export * from './xrpIO/ripple-binding'
|
||||
__exportStar(require("./util/types"), exports);
|
||||
__exportStar(require("./util/protocol.constants"), exports);
|
||||
//export * from 'ripple-lib'
|
||||
//export { RippleAPI } from 'ripple-lib'
|
||||
__exportStar(require("./xrpIO/xrpl-binding"), exports);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare const ERR_BAD_TX_HASH: (hash: string) => Error;
|
||||
export declare const ERR_NO_VERIFY_OWNER: (hash: string, actualAccount: string, desiredAccount: string) => Error;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ERR_NO_VERIFY_OWNER = exports.ERR_BAD_TX_HASH = void 0;
|
||||
const ERR_BAD_TX_HASH = (hash) => new Error(`Bad tx hash format: "${hash}"`);
|
||||
exports.ERR_BAD_TX_HASH = ERR_BAD_TX_HASH;
|
||||
const ERR_NO_VERIFY_OWNER = (hash, actualAccount, desiredAccount) => new Error(`Expected tx "${hash}" to be initiated by ${desiredAccount} but was ${actualAccount}`);
|
||||
exports.ERR_NO_VERIFY_OWNER = ERR_NO_VERIFY_OWNER;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export declare const MSG_DELIM: string;
|
||||
export declare const MSG_DATA_MAX: number;
|
||||
export declare const PUBKEY_LEN: number;
|
||||
export declare const NON_ZERO_TX_HASH: RegExp;
|
||||
export declare const PTR_FORMAT: RegExp;
|
||||
export declare const DATA_FORMAT: RegExp;
|
||||
export declare const SIGNATURE_FORMAT: RegExp;
|
||||
export declare const SIGNER_FORMAT: RegExp;
|
||||
export declare const MSG_FORMAT: RegExp;
|
||||
export declare const AMOUNT_DECIMALS = 18;
|
||||
export declare const MAX_SUPPLY = 20000000;
|
||||
export declare const AMOUNT_FORMAT: RegExp;
|
||||
export declare const MIN_XRP_FEE = "0.00001";
|
||||
export declare const MIN_XRP_TX_VALUE = "0.000001";
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MIN_XRP_TX_VALUE = exports.MIN_XRP_FEE = exports.AMOUNT_FORMAT = exports.MAX_SUPPLY = exports.AMOUNT_DECIMALS = exports.MSG_FORMAT = exports.SIGNER_FORMAT = exports.SIGNATURE_FORMAT = exports.DATA_FORMAT = exports.PTR_FORMAT = exports.NON_ZERO_TX_HASH = exports.PUBKEY_LEN = exports.MSG_DATA_MAX = exports.MSG_DELIM = void 0;
|
||||
exports.MSG_DELIM = ' ';
|
||||
exports.MSG_DATA_MAX = 925;
|
||||
exports.PUBKEY_LEN = 66;
|
||||
exports.NON_ZERO_TX_HASH = new RegExp(`[0-9A-F]{64}`);
|
||||
exports.PTR_FORMAT = new RegExp(`^((${exports.NON_ZERO_TX_HASH.source})|0)`);
|
||||
exports.DATA_FORMAT = new RegExp(`(.{1,${exports.MSG_DATA_MAX}})`);
|
||||
exports.SIGNATURE_FORMAT = new RegExp(`(\\S{140}|\\S{142})$`);
|
||||
exports.SIGNER_FORMAT = new RegExp(`(\\S{${exports.PUBKEY_LEN}})`);
|
||||
exports.MSG_FORMAT = new RegExp(`${exports.PTR_FORMAT.source}${exports.MSG_DELIM}${exports.DATA_FORMAT.source}`, 'm');
|
||||
exports.AMOUNT_DECIMALS = 18;
|
||||
exports.MAX_SUPPLY = 20_000_000;
|
||||
exports.AMOUNT_FORMAT = new RegExp(`\d+(\.\d{1,${exports.AMOUNT_DECIMALS}})?`);
|
||||
exports.MIN_XRP_FEE = "0.00001";
|
||||
exports.MIN_XRP_TX_VALUE = "0.000001";
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
export declare type Memo = {
|
||||
type?: string;
|
||||
format?: string;
|
||||
data?: string;
|
||||
};
|
||||
export declare type Wallet = {
|
||||
secret: string;
|
||||
address: string;
|
||||
};
|
||||
export declare type Signature = {
|
||||
signature: string;
|
||||
signer: PublicKey;
|
||||
};
|
||||
export declare type Address = string;
|
||||
export declare type Secret = string;
|
||||
export declare type PublicKey = string;
|
||||
export declare type Amount = number;
|
||||
export declare type TxHash = string;
|
||||
export declare type Options = {
|
||||
debug?: boolean;
|
||||
connectionTimeout?: number;
|
||||
readMaxRetry?: number;
|
||||
readRetryTimeout?: number;
|
||||
};
|
||||
export declare const defaultOptions: {
|
||||
debug: boolean;
|
||||
connectionTimeout: number;
|
||||
readFreshApi: boolean;
|
||||
readMaxRetry: number;
|
||||
readRetryTimeout: number;
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.defaultOptions = void 0;
|
||||
exports.defaultOptions = {
|
||||
debug: false,
|
||||
connectionTimeout: 100000,
|
||||
readFreshApi: true,
|
||||
readMaxRetry: -1,
|
||||
readRetryTimeout: 1000
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { Memo, Wallet } from '../util/types';
|
||||
import { RippleAPI } from 'ripple-lib';
|
||||
export declare const getLatestSequence: (api: RippleAPI, accountAddress: string) => Promise<number>;
|
||||
export declare const sendPayment: (api: RippleAPI, data: Memo[], from: string, to: string, secret: string, sequence: number) => Promise<import("ripple-lib/dist/npm/transaction/submit").FormattedSubmitResponse>;
|
||||
export declare const getTransactions: (api: RippleAPI, address: string, minLedgerVersion?: number) => Promise<any[]>;
|
||||
export declare const writeRaw: (api: RippleAPI, data: Memo, from: string, to: string, secret: string, sequence?: number) => Promise<string>;
|
||||
export declare const readRaw: (api: RippleAPI, hash: string) => Promise<Memo>;
|
||||
export declare const subscribe: (api: RippleAPI, address: string, callback: (tx: any) => any) => Promise<void>;
|
||||
export declare const treeWrite: (api: RippleAPI, data: string, wallet: Wallet, to: string, format?: 'L' | 'N') => Promise<string>;
|
||||
export declare const treeRead: (api: RippleAPI, hashes: string[]) => Promise<string>;
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.treeRead = exports.treeWrite = exports.subscribe = exports.readRaw = exports.writeRaw = exports.getTransactions = exports.sendPayment = exports.getLatestSequence = void 0;
|
||||
const protocol_constants_1 = require("../util/protocol.constants");
|
||||
const ripple_lib_1 = require("ripple-lib");
|
||||
const zlib = __importStar(require("zlib"));
|
||||
const util = __importStar(require("util"));
|
||||
const chunkString = (str, length) => str.match(new RegExp('.{1,' + length + '}', 'gs'));
|
||||
const PAYLOAD_SIZE = 925;
|
||||
const debug = false;
|
||||
const cloneApi = async (api) => {
|
||||
try {
|
||||
const subApi = new ripple_lib_1.RippleAPI({ server: api.connection['_url'] });
|
||||
await subApi.connect();
|
||||
return subApi;
|
||||
}
|
||||
catch (e) {
|
||||
if (debug) {
|
||||
console.log("CLONEAPI ERR", e);
|
||||
}
|
||||
return await cloneApi(api);
|
||||
}
|
||||
};
|
||||
const getLatestSequence = async (api, accountAddress) => {
|
||||
if (debug)
|
||||
console.log("Getting acc info for", accountAddress);
|
||||
const accountInfo = await api.getAccountInfo(accountAddress, {});
|
||||
return Number(accountInfo.sequence - 1);
|
||||
};
|
||||
exports.getLatestSequence = getLatestSequence;
|
||||
const compressB64 = async (data) => (await util.promisify(zlib.deflate)(Buffer.from(data, 'utf-8'))).toString('base64');
|
||||
const decompressB64 = async (data) => (await util.promisify(zlib.inflate)(Buffer.from(data, 'base64'))).toString('utf-8');
|
||||
const sendReliably = (api, signed, preparedPayment) => new Promise((res, rej) => {
|
||||
const ledgerClosedCallback = async (event) => {
|
||||
let status;
|
||||
try {
|
||||
status = await api.getTransaction(signed.id, {
|
||||
minLedgerVersion: 25235454
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
// Typical error when the tx hasn't been validated yet:
|
||||
if (e.name !== 'MissingLedgerHistoryError') {
|
||||
//console.log(e)
|
||||
}
|
||||
if (event.ledger_index > preparedPayment.instructions.maxLedgerVersion + 3) {
|
||||
// Assumptions:
|
||||
// - We are still connected to the same rippled server
|
||||
// - No ledger gaps occurred
|
||||
// - All ledgers between the time we submitted the tx and now have been checked for the tx
|
||||
status = {
|
||||
finalResult: 'Transaction was not, and never will be, included in a validated ledger'
|
||||
};
|
||||
return rej(status);
|
||||
}
|
||||
else {
|
||||
// Check again later:
|
||||
api.connection.once('ledgerClosed', ledgerClosedCallback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return res(status);
|
||||
};
|
||||
api.connection.once('ledgerClosed', ledgerClosedCallback);
|
||||
});
|
||||
const sendPayment = async (api, data, from, to, secret, sequence) => {
|
||||
if (debug)
|
||||
console.log("Sending payment with seq", sequence);
|
||||
const options = {
|
||||
maxLedgerVersionOffset: 5,
|
||||
fee: protocol_constants_1.MIN_XRP_FEE,
|
||||
sequence: sequence,
|
||||
};
|
||||
const payment = {
|
||||
source: {
|
||||
address: from,
|
||||
maxAmount: {
|
||||
value: protocol_constants_1.MIN_XRP_TX_VALUE,
|
||||
currency: 'XRP'
|
||||
},
|
||||
},
|
||||
destination: {
|
||||
address: to,
|
||||
amount: {
|
||||
value: protocol_constants_1.MIN_XRP_TX_VALUE,
|
||||
currency: 'XRP'
|
||||
},
|
||||
},
|
||||
memos: data,
|
||||
};
|
||||
const _api = await cloneApi(api);
|
||||
try {
|
||||
const prepared = await _api.preparePayment(from, payment, options);
|
||||
const signed = _api.sign(prepared.txJSON, secret);
|
||||
const txHash = await _api.submit(signed.signedTransaction);
|
||||
//if(debug) console.log("Transaction submitted", txHash)
|
||||
await sendReliably(_api, signed, prepared);
|
||||
return txHash;
|
||||
}
|
||||
catch (error) {
|
||||
if (debug)
|
||||
console.log("SENDPAYMENT ERROR", error);
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
_api.disconnect();
|
||||
}
|
||||
};
|
||||
exports.sendPayment = sendPayment;
|
||||
const getTransactions = async (api, address, minLedgerVersion = 25235454) => {
|
||||
const txs = await api.getTransactions(address, {
|
||||
minLedgerVersion: minLedgerVersion,
|
||||
earliestFirst: true,
|
||||
excludeFailures: true,
|
||||
});
|
||||
return txs;
|
||||
};
|
||||
exports.getTransactions = getTransactions;
|
||||
const writeRaw = async (api, data, from, to, secret, sequence) => {
|
||||
//if (memoSize(data) > 1000) throw new Error("data length exceeds capacity")
|
||||
try {
|
||||
if (!sequence) {
|
||||
const accountInfo = await (0, exports.getLatestSequence)(api, from);
|
||||
sequence = accountInfo + 1;
|
||||
}
|
||||
const resp = await (0, exports.sendPayment)(api, [data], from, to, secret, sequence);
|
||||
return resp['tx_json'].hash;
|
||||
}
|
||||
catch (error) {
|
||||
if (debug) {
|
||||
console.log("WRITERAW ERR", error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
exports.writeRaw = writeRaw;
|
||||
const readRaw = async (api, hash) => {
|
||||
api = await cloneApi(api);
|
||||
let tx;
|
||||
try {
|
||||
tx = await api.getTransaction(hash, {
|
||||
minLedgerVersion: 25235454
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
// if(debug){
|
||||
console.log("READRAW ERR", e);
|
||||
api.isConnected;
|
||||
// }
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
await api.disconnect();
|
||||
}
|
||||
if (!tx || !tx.specification || !tx.specification['memos'] || !tx.specification['memos'][0]) {
|
||||
console.log(tx);
|
||||
throw new Error('Invalid Transaction ' + hash);
|
||||
}
|
||||
return tx.specification['memos'][0];
|
||||
};
|
||||
exports.readRaw = readRaw;
|
||||
const subscribe = async (api, address, callback) => {
|
||||
api.connection.on('transaction', (tx) => callback(tx));
|
||||
await api.connection.request({
|
||||
command: 'subscribe',
|
||||
accounts: [address],
|
||||
});
|
||||
};
|
||||
exports.subscribe = subscribe;
|
||||
const treeWrite = async (api, data, wallet, to, format = 'L') => {
|
||||
data = await compressB64(data);
|
||||
const chunks = chunkString(data, PAYLOAD_SIZE);
|
||||
const latestSequence = await (0, exports.getLatestSequence)(api, wallet.address);
|
||||
const hashes = await Promise.all(Object.entries(chunks).map(([i, chunk]) => (0, exports.writeRaw)(api, { data: chunk, format: format }, wallet.address, to, wallet.secret, latestSequence + Number(i) + 1)));
|
||||
if (hashes.length === 1) {
|
||||
return hashes[0];
|
||||
}
|
||||
return await (0, exports.treeWrite)(api, JSON.stringify(hashes), wallet, to, 'N');
|
||||
};
|
||||
exports.treeWrite = treeWrite;
|
||||
const treeRead = async (api, hashes) => {
|
||||
const memos = await Promise.all(hashes.map(hash => (0, exports.readRaw)(api, hash)));
|
||||
const payload = await decompressB64(memos.map(memo => memo.data).join(''));
|
||||
if (memos.some(memo => memo.format === 'N')) {
|
||||
return await (0, exports.treeRead)(api, JSON.parse(payload));
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
exports.treeRead = treeRead;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { Memo, Options } from '../util/types';
|
||||
export declare class xrpIO {
|
||||
private server;
|
||||
private options;
|
||||
private api;
|
||||
constructor(server: string, options?: Options);
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
private cloneApi;
|
||||
private sendPayment;
|
||||
writeRaw(data: Memo, to: string, secret: string, sequence?: number, amount?: string): Promise<string>;
|
||||
private getTransaction;
|
||||
readRaw(hash: string, verifyOwner?: string): Promise<Memo>;
|
||||
treeWrite(data: string, to: string, secret: string, format?: 'L' | 'N'): Promise<string>;
|
||||
treeRead(hashes: string[], verifyOwner?: string): Promise<string>;
|
||||
getAccountSequence(address: string): Promise<number>;
|
||||
estimateFee(data: string, denomination?: 'XRP' | 'DROPS', cost?: number): Promise<number>;
|
||||
xrpToDrops(xrp: number): number;
|
||||
dropsToXrp(drops: number): number;
|
||||
private dbg;
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.xrpIO = void 0;
|
||||
const types_1 = require("../util/types");
|
||||
const xrpl_1 = require("xrpl");
|
||||
const zlib = __importStar(require("zlib"));
|
||||
const util = __importStar(require("util"));
|
||||
const protocol_constants_1 = require("../util/protocol.constants");
|
||||
const errors_1 = require("../util/errors");
|
||||
const compressB64 = async (data) => (await util.promisify(zlib.deflate)(Buffer.from(data, 'utf-8'))).toString('base64');
|
||||
const decompressB64 = async (data) => (await util.promisify(zlib.inflate)(Buffer.from(data, 'base64'))).toString('utf-8');
|
||||
const hexDecode = (str) => Buffer.from(str, 'hex').toString('utf8');
|
||||
const hexEncode = (str) => Buffer.from(str, 'utf8').toString('hex').toUpperCase();
|
||||
const chunkString = (str, length) => str.match(new RegExp('.{1,' + length + '}', 'gs'));
|
||||
const genRandHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
|
||||
const PAYLOAD_SIZE = 925;
|
||||
const XRP_PER_DROP = 0.000001;
|
||||
const DROP_PER_XRP = 1000000;
|
||||
class xrpIO {
|
||||
server;
|
||||
options;
|
||||
api;
|
||||
constructor(server, options = types_1.defaultOptions) {
|
||||
this.server = server;
|
||||
this.options = options;
|
||||
this.options.debug = options.debug ? Boolean(options.debug) : types_1.defaultOptions.debug;
|
||||
this.options.connectionTimeout = options.connectionTimeout ? Number(options.connectionTimeout) : types_1.defaultOptions.connectionTimeout;
|
||||
this.options.readMaxRetry = options.readMaxRetry ? Number(options.readMaxRetry) : types_1.defaultOptions.readMaxRetry;
|
||||
this.options.readRetryTimeout = options.readRetryTimeout ? Number(options.readRetryTimeout) : types_1.defaultOptions.readRetryTimeout;
|
||||
this.api = new xrpl_1.Client(server, {
|
||||
connectionTimeout: this.options.connectionTimeout
|
||||
});
|
||||
}
|
||||
async connect() {
|
||||
if (!this.api.isConnected())
|
||||
await this.api.connect();
|
||||
}
|
||||
async disconnect() {
|
||||
try {
|
||||
await this.api.disconnect();
|
||||
}
|
||||
catch (e) {
|
||||
console.log("DISCONNECT ERROR", e);
|
||||
}
|
||||
}
|
||||
async cloneApi() {
|
||||
let _api = new xrpl_1.Client(this.server, {
|
||||
connectionTimeout: this.options.connectionTimeout
|
||||
});
|
||||
while (!_api.isConnected()) {
|
||||
try {
|
||||
await _api.connect();
|
||||
return _api;
|
||||
}
|
||||
catch (e) {
|
||||
this.dbg('CLONEAPI ERR', 'Connection failed', String(e['message']));
|
||||
await _api.disconnect();
|
||||
_api = new xrpl_1.Client(this.server, {
|
||||
connectionTimeout: this.options.connectionTimeout
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
async sendPayment(data, to, secret, sequence, amount = "1") {
|
||||
const wallet = xrpl_1.Wallet.fromSecret(secret);
|
||||
this.dbg("Sending payment", wallet.address, '->', to);
|
||||
const _api = await this.cloneApi();
|
||||
try {
|
||||
const payment = await _api.autofill({
|
||||
TransactionType: 'Payment',
|
||||
Account: wallet.address,
|
||||
Destination: to,
|
||||
Sequence: sequence,
|
||||
Amount: amount,
|
||||
Memos: [{
|
||||
Memo: {
|
||||
MemoData: hexEncode(data.data || ""),
|
||||
MemoFormat: hexEncode(data.format || ""),
|
||||
MemoType: hexEncode(data.type || "")
|
||||
}
|
||||
}]
|
||||
});
|
||||
const response = await _api.submitAndWait(payment, { wallet });
|
||||
await _api.disconnect();
|
||||
this.dbg("Tx finalized", response.result.hash, response.result.Sequence);
|
||||
return response;
|
||||
}
|
||||
catch (error) {
|
||||
this.dbg("SENDPAYMENT ERROR", error);
|
||||
await _api.disconnect();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async writeRaw(data, to, secret, sequence, amount = "1") {
|
||||
this.dbg("Writing data", data);
|
||||
const tx = await this.sendPayment(data, to, secret, sequence, amount);
|
||||
return tx.result.hash;
|
||||
}
|
||||
async getTransaction(hash, retry = 0) {
|
||||
this.dbg("Getting Tx", hash);
|
||||
try {
|
||||
return await this.api.request({
|
||||
command: 'tx',
|
||||
transaction: hash,
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
this.dbg(e);
|
||||
if (this.options.readMaxRetry != -1) {
|
||||
if (retry >= this.options.readMaxRetry)
|
||||
console.error("Retry limit exceeded for", hash, ". This is an irrecoverable error");
|
||||
throw e;
|
||||
}
|
||||
await new Promise(res => setTimeout(res, this.options.readRetryTimeout));
|
||||
return await this.getTransaction(hash, retry + 1);
|
||||
}
|
||||
}
|
||||
async readRaw(hash, verifyOwner) {
|
||||
if (!protocol_constants_1.NON_ZERO_TX_HASH.test(hash)) {
|
||||
throw (0, errors_1.ERR_BAD_TX_HASH)(hash);
|
||||
}
|
||||
const tx = await this.getTransaction(hash);
|
||||
if (verifyOwner && tx.result.Account != verifyOwner) {
|
||||
throw (0, errors_1.ERR_NO_VERIFY_OWNER)(hash, tx.result.Account, verifyOwner);
|
||||
}
|
||||
const memo = tx.result.Memos[0].Memo;
|
||||
const memoParsed = {
|
||||
data: hexDecode(memo.MemoData),
|
||||
format: hexDecode(memo.MemoFormat),
|
||||
type: hexDecode(memo.MemoType)
|
||||
};
|
||||
this.dbg(hash, "data", memoParsed);
|
||||
return memoParsed;
|
||||
}
|
||||
async treeWrite(data, to, secret, format = 'L') {
|
||||
const wallet = xrpl_1.Wallet.fromSecret(secret);
|
||||
data = await compressB64(data);
|
||||
const chunks = chunkString(data, PAYLOAD_SIZE);
|
||||
const latestSequence = await this.getAccountSequence(wallet.address);
|
||||
const hashes = await Promise.all(Object.entries(chunks).map(([i, chunk]) => this.writeRaw({ data: chunk, format: format }, to, secret, latestSequence + Number(i))));
|
||||
if (hashes.length === 1) {
|
||||
return hashes[0];
|
||||
}
|
||||
return await this.treeWrite(JSON.stringify(hashes), to, secret, 'N');
|
||||
}
|
||||
async treeRead(hashes, verifyOwner) {
|
||||
const bad_hash = hashes.find(hash => !protocol_constants_1.NON_ZERO_TX_HASH.test(hash));
|
||||
if (bad_hash)
|
||||
throw (0, errors_1.ERR_BAD_TX_HASH)(bad_hash);
|
||||
const memos = await Promise.all(hashes.map(hash => this.readRaw(hash, verifyOwner)));
|
||||
const payload = await decompressB64(memos.map(memo => memo.data).join(''));
|
||||
if (memos.some(memo => memo.format === 'N')) {
|
||||
return await this.treeRead(JSON.parse(payload), verifyOwner);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
async getAccountSequence(address) {
|
||||
this.dbg("Getting acc info for", address);
|
||||
const accountInfo = await this.api.request({
|
||||
command: 'account_info',
|
||||
account: address,
|
||||
strict: true,
|
||||
});
|
||||
this.dbg("Got account_info", accountInfo);
|
||||
return Number(accountInfo.result.account_data.Sequence);
|
||||
}
|
||||
async estimateFee(data, denomination = 'DROPS', cost = 0) {
|
||||
data = await compressB64(data);
|
||||
const chunks = chunkString(data, PAYLOAD_SIZE);
|
||||
if (chunks.length === 1) {
|
||||
return (denomination === "DROPS" ? (cost + 1) : this.dropsToXrp(cost + 1));
|
||||
}
|
||||
return this.estimateFee(JSON.stringify(chunks.map(_ => genRandHex(64))), denomination, cost + chunks.length);
|
||||
}
|
||||
xrpToDrops(xrp) {
|
||||
return xrp * DROP_PER_XRP;
|
||||
}
|
||||
dropsToXrp(drops) {
|
||||
return drops * XRP_PER_DROP;
|
||||
}
|
||||
dbg(...args) {
|
||||
if (this.options.debug) {
|
||||
console.log.apply(console, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.xrpIO = xrpIO;
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "xrpio",
|
||||
"version": "0.2.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.nitowa.xyz/npm-packages/xrpio.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://gitea.nitowa.xyz/npm-packages/xrpio/issues",
|
||||
"email": "peter.millauer@gmail.com"
|
||||
},
|
||||
"homepage": "https://gitea.nitowa.xyz/docs/xrpio",
|
||||
"description": "XRP arbitrary data writer and reader",
|
||||
"main": "lib/src/index.js",
|
||||
"files": [
|
||||
"lib/src",
|
||||
"lib/browser"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rm -rf gui/main.js gui/index.html gateway/main.js build lib docs",
|
||||
"start": "npm run build && npm run launch",
|
||||
"launch": "node ./lib/Launcher.js",
|
||||
"tsc": "tsc",
|
||||
"build": "npm run clean && tsc && npm run webpack",
|
||||
"build-all": "npm run build && npm run webpack-gui",
|
||||
"webpack-gui": "webpack --config webpack.gui.js --progress && cp build/gui/main.js gui",
|
||||
"webpack-gateway": "webpack --config webpack.gateway.js --progress && cp build/gateway/main.js gateway",
|
||||
"test": "npm run build && mocha --bail=true ./lib/test/*.js",
|
||||
"deploy": "node ./lib/Deploy.js",
|
||||
"webpack": "webpack --config ./src/webpack.js",
|
||||
"docs": "typedoc --out docs --readme ./README.md --plugin typedoc-plugin-markdown --hideBreadcrumbs ./src/index.ts"
|
||||
},
|
||||
"author": "nitowa",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ripple-lib": "^1.10.0",
|
||||
"xrpl": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/mocha": "^8.2.2",
|
||||
"@types/node": "^14.14.37",
|
||||
"base-64": "^1.0.0",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"chai": "^4.3.4",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"mocha": "^9.2.0",
|
||||
"net": "^1.0.2",
|
||||
"node-fetch": "^2.6.2",
|
||||
"process": "^0.11.10",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.2.0",
|
||||
"tls": "^0.0.1",
|
||||
"ts-loader": "^8.1.0",
|
||||
"typedoc": "^0.22.11",
|
||||
"typedoc-plugin-markdown": "^3.11.12",
|
||||
"typescript": "^4.5.0",
|
||||
"url": "^0.11.0",
|
||||
"utf8": "^3.0.0",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.0",
|
||||
"wtfnode": "^0.9.1"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user