lib/tupelo.js
var cbor = require("cbor");
var tupelo = require("tupelo-messages");
var transactions = tupelo.transactions;
var services = tupelo.services;
var rpcclient = tupelo.rpcclient;
var grpc = tupelo.grpc;
const CHAINTREE_DATA_PATH = "/tree/data";
const promiseAroundRpcCallback = (toExec) => {
return new Promise((resolve, reject) => {
var clbk = (err, response) => {
if (err == null) {
resolve(response);
} else {
reject(err);
}
};
toExec(clbk);
});
};
const walletCredentials = (credObj) => {
var creds = new services.Credentials();
creds.setWalletName(credObj.walletName);
creds.setPassPhrase(credObj.passPhrase);
return creds;
};
const setOwnershipPayload = (newOwnerKeys) => {
var payload = new transactions.SetOwnershipPayload();
payload.setAuthenticationList(newOwnerKeys);
return payload;
};
const setOwnershipTransaction = (newOwnerKeys) => {
var payload = setOwnershipPayload(newOwnerKeys);
var txn = new transactions.Transaction();
txn.setType(transactions.TransactionType.SETOWNERSHIP);
txn.setSetOwnershipPayload(payload);
return txn;
};
const setDataPayload = (path, value) => {
var cborData = cbor.Encoder.encode(value);
var payload = new transactions.SetDataPayload();
payload.setPath(path);
payload.setValue(cborData);
return payload;
};
const setDataTransaction = (path, value) => {
var payload = setDataPayload(path, value);
var txn = new transactions.Transaction();
txn.setType(transactions.TransactionType.SETDATA);
txn.setSetDataPayload(payload);
return txn;
};
const establishTokenPayload = (name, maximum) => {
var policy = new transactions.TokenMonetaryPolicy();
policy.setMaximum(maximum);
var payload = new transactions.EstablishTokenPayload();
payload.setName(name);
payload.setMonetaryPolicy(policy);
return payload;
};
const establishTokenTransaction = (name, maximum) => {
var payload = establishTokenPayload(name, maximum);
var txn = new transactions.Transaction();
txn.setType(transactions.TransactionType.ESTABLISHTOKEN);
txn.setEstablishTokenPayload(payload);
return txn;
};
const mintTokenPayload = (name, amount) => {
var payload = new transactions.MintTokenPayload();
payload.setName(name);
payload.setAmount(amount);
return payload;
};
const mintTokenTransaction = (name, amount) => {
var payload = mintTokenPayload(name, amount);
var txn = new transactions.Transaction();
txn.setType(transactions.TransactionType.MINTTOKEN);
txn.setMintTokenPayload(payload);
return txn;
};
const sendTokenPayload = (sendId, name, amount, destinationChainId) => {
var payload = new transactions.SendTokenPayload();
payload.setId(sendId);
payload.setName(name);
payload.setAmount(amount);
payload.setDestination(destinationChainId);
return payload;
};
const sendTokenTransaction = (sendId, name, amount, destinationChainId) => {
var payload = sendTokenPayload(sendId, name, amount, destinationChainId);
var txn = new transactions.Transaction();
txn.setType(transactions.TransactionType.SENDTOKEN);
txn.setSendTokenPayload(payload);
return txn;
};
const receiveTokenPayload = (sendId, tip, signature, leaves) => {
var payload = new transactions.ReceiveTokenPayload();
payload.setSendTokenTransactionId(sendId);
payload.setTip(tip);
payload.setSignature(signature);
payload.setLeaves(leaves);
return payload;
};
const receiveTokenTransaction = (sendId, tip, signature, leaves) => {
var payload = receiveTokenPayload(sendId, tip, signature, leaves);
var txn = new transactions.Transaction();
txn.setType(transactions.TransactionType.RECEIVETOKEN);
txn.setReceiveTokenPayload(payload);
return txn;
};
/**
* Connect to a Tupelo wallet managed by a remote Tupelo RPC server.
*
* @param {string} walletServer - "host:port" string of the RPC wallet server
* @param {Object} walletCreds - Credentials for the connecting wallet
* @param {string} walletCreds.walletName - Wallet name
* @param {string} walletCreds.passPhrase - Wallet passphrase
*
* @return {TupeloClient} Tupelo client connection.
*/
const connect = (walletServer, walletCreds) => {
return new TupeloClient(walletServer, walletCreds);
};
/**
* Represents a connection to a specific Tupelo wallet managed by a remote
* Tupelo RPC server
*/
class TupeloClient {
constructor(walletServer, walletCreds) {
/**
* The URL ("host:port") of the RPC wallet server to connect to
*
* @type {string}
*/
this.walletServer = walletServer;
/**
* The name and passphrase of the wallet to connect to
*
* @type {WalletCredentials}
*/
this.walletCreds = walletCredentials(walletCreds);
/**
* @typedef {Object} WalletCredentials
* @property {string} walletName - Wallet name
* @property {string} passPhrase - Wallet passphrase
*/
/**
* Back end Tupelo Wallet RPC service connection
*
* @type {WalletRPCService}
*/
this.rpc = new rpcclient
.WalletRPCServiceClient(walletServer, grpc.credentials.createInsecure());
}
/**
* Register a new wallet with the client credentials.
*
* @return {Promise<RegisterResponse, RpcError>}
*/
register() {
return promiseAroundRpcCallback((clbk) => {
var req = new services.RegisterWalletRequest();
req.setCreds(this.walletCreds);
this.rpc.register(req, clbk);
});
}
/**
* @typedef {Object} RegisterResponse
* @property {string} walletName - The name of the newly registered wallet
*/
/**
* Generate a new chain tree ownership key pair.
*
* @return {Promise<GenerateKeyResponse, RpcError>}
*/
generateKey() {
return promiseAroundRpcCallback((clbk) => {
var req = new services.GenerateKeyRequest();
req.setCreds(this.walletCreds);
this.rpc.generateKey(req, clbk);
});
}
/**
* @typedef {Object} GenerateKeyResponse
* @property {string} keyAddr - Public key address
*/
/**
* List the addresses of the keys associated with the connected wallet.
*
* @return {Promise<ListKeysResponse, RpcError>}
*/
listKeys() {
return promiseAroundRpcCallback((clbk) => {
var req = new services.ListKeysRequest();
req.setCreds(this.walletCreds);
this.rpc.listKeys(req, clbk);
});
}
/**
* @typedef {Object} ListKeysResponse
* @property {string[]} keyAddrs - Public key addresses associated with the connected wallet.
*/
/**
* Create a new chain tree owned by the key at `keyAddr`.
*
* @param {string} keyAddr - Address of the key that owns the new chain tree.
* @param {StorageAdapterConfig} [storageAdapter] - Storage configuration for the chain tree
* @return {Promise<CreateChainResponse, RpcError>}
*/
createChainTree(keyAddr, storageAdapter) {
return promiseAroundRpcCallback((clbk) => {
var req = new services.GenerateChainResponse();
req.setCreds(this.walletCreds);
req.setKeyAddr(keyAddr);
req.setStorageAdapter(storageAdapter);
this.rpc.createChainTree(req, clbk);
});
}
/**
* @typedef {Object} CreateChainResponse
* @property {string} chainId - The ID of the new chain tree
*/
/**
* Get a Base58 serialized chain tree Export
*
* @param {string} chainId - The ID of the chain tree to be exported
*
* @return {Promise<ExportChainTreeResponse, RpcError>}
*/
exportChainTree(chainId) {
var req = new services.ExportChainRequest();
req.setCreds(this.walletCreds);
req.setChainId(chainId);
return promiseAroundRpcCallback((clbk) => {
this.rpc.exportChainTree(req, clbk);
});
}
/**
* @typedef {Object} ExportChainTreeResponse
* @property {SerializedChainTree} chainTree - The serialized chain tree
*/
/**
* Import a serialized chain tree and save it to the wallet.
*
* @param {SerializedChaintree} chainTree - Serialized chain tree to import
* @param {StorageAdapterConfig} [storageAdapter] - Storage configuration for the chain tree
*
*
* StorageAdapterConfig
*/
importChainTree(chainTree, storageAdapter) {
var req = new services.ImportChainRequest();
req.setCreds(this.walletCreds);
req.setChainTree(chainTree);
return promiseAroundRpcCallback((clbk) => {
this.rpc.importChainTree(req, clbk);
});
}
/**
* List the IDs of the chain trees associated with the connected wallet.
*
* @return {Promise<ListChainIdsResponse, RpcError>}
*/
listChainIds() {
var req = new services.ListChainIdsRequest();
req.setCreds(this.walletCreds);
return promiseAroundRpcCallback((clbk) => {
this.rpc.listChainIds(req, clbk);
});
}
/**
* @typedef {Object} ListChainIdsResponse
* @property {string[]} chainIds - Chain tree IDs associated with this wallet.
*/
/**
* Get the latest tip (as known by the Tupelo network signers) of the chain
* tree with id `chainId`
*
* @param {string} chainId - The ID of the chain tree.
*
* @return {Promise<GetTipResponse, RpcError>}
*/
getTip(chainId) {
var req = new services.GetTipRequest();
req.setCreds(this.walletCreds);
req.setChainId(chainId);
return promiseAroundRpcCallback((clbk) => {
this.rpc.getTip(req, clbk);
});
}
/**
* @typedef {Object} GetTipResponse
* @property {string} tip - The chain tree tip as known by the Tupelo signers
*/
/**
* Apply a sequence of transactions to a chain tree
*
* @param {string} chainId - The ID of the chain tree to store the data on.
* @param {string} keyAddr - Address of the key that owns the chain tree.
* @param {Object[]} transactions - List of transactions to apply
*
* @return {Promise<PlayTransactionsResponse, RpcError>}
*/
playTransactions(chainId, keyAddr, transactions) {
var req = new services.PlayTransactionsRequest();
req.setCreds(this.walletCreds);
req.setChainId(chainId);
req.setTransactions(this.transactions);
return promiseAroundRpcCallback((clbk) => {
this.rpc.playTransactions(req, clbk);
});
}
/**
* @typedef {Object} PlayTransactionsResponse
* @property {string} tip - The chain tree tip as known by the Tupelo signers after the transactions are applied
*/
/**
* Store data on a chain tree with a transaction validated by the network's
* notary group.
*
* @param {string} chainId - The ID of the chain tree to store the data on.
* @param {string} keyAddr - Address of the key that owns the chain tree.
* @param {string} path - '/' delimited path into the chain tree to store the data
* @param {string|number|boolean|Array|Object} value - The data to store.
*
* @return {Promise<SetDataResponse, RpcError>}
*/
setData(chainId, keyAddr, path, value) {
var transaction = setDataTransaction(path, value);
return this.playTransactions(chainId, keyAddr, [transaction]);
}
/**
* @typedef {Object} SetDataResponse
* @property {string} tip - The chain tree tip after the transaction.
*/
/**
* Resolve data from the root of the chain tree.
*
* The data will be resolved in the tree according to the path until we reach the end
* of the path or a leaf node. If we reach a leaf node before the end of the path,
* the data at the leaf node is returned and the remaining unresolved part of the path is
* set as the response's `remainingPath` property.
* @param {string} chainId - The ID of the chain tree to retrieve the data from.
* @param {string} path - '/' delimited path into the chain tree where the data is stored.
*
* @return {Promise<ResolveResponse, RpcError>}
*/
resolve(chainId, path) {
return new Promise((resolve, reject) => {
var clbk = (err, response) => {
if (err == null) {
cbor.Decoder.decodeAll(response.data)
.then((decoded) => {
response.data = decoded;
if (response.remainingPath == null) {
response.remainingPath = '';
}
resolve(response);
},(err) => {
reject(err);
});
} else {
reject(err);
}
};
var req = new services.ResolveRequest();
req.setCreds(this.walletCreds);
req.setChainId(chainId);
req.setPath(path);
this.rpc.resolve(req, clbk);
});
}
/**
* @typedef {Object} ResolveResponse
* @property {string} remainingPath - The path remaining after retrieving the data
* @property {Object} data - The data retrieved
*/
/**
* Resolve data from the user portion ("/tree/data") of the chain tree.
*
* The data will be resolved in the tree according to the path until we reach the end
* of the path or a leaf node. If we reach a leaf node before the end of the path,
* the data at the leaf node is returned and the remaining unresolved part of the path is
* set as the response's `remainingPath` property.
* @param {string} chainId - The ID of the chain tree to retrieve the data from.
* @param {string} path - '/' delimited path inside of tree/data where the data is stored.
*
* @return {Promise<ResolveResponse, RpcError>}
*/
resolveData(chainId, path) {
let resolvePath = CHAINTREE_DATA_PATH;
let trimmedPath = path ? path.replace(/^\/+/g, '') : path;
if (trimmedPath) {
resolvePath += "/" + trimmedPath;
}
return this.resolve(chainId, resolvePath);
}
/**
* Resolve data from a tip of a chain tree given a certain path.
*
* @param {string} chainId - The ID of the chain tree to retrieve the data from.
* @param {string} path - '/' delimited path into the chain tree where the data is stored
* @param {string} tip - The tip in question.
*
* @return {Promise<ResolveResponse, RpcError>}
*/
async resolveAt(chainId, path, tip) {
const response = await new Promise((resolve, reject) => {
var req = new services.ResolveAtRequest();
req.setCreds(this.walletCreds);
req.setChainId(this.chainId);
req.setPath(this.path);
req.setTip(this.tip);
this.rpc.resolveAt(req, (err, response) => {
if (err == null) {
resolve(response);
} else {
reject(err);
}
});
});
const decoded = await cbor.Decoder.decodeAll(response.data);
response.data = decoded;
return response;
}
/**
* Define the set of owners of the chain tree with ID `chainId` as the set of keys in
* `newOwnerKeys` in a transaction, and register that transaction with the notary group.
*
* @param {string} chainId - The ID of the chain tree.
* @param {string} keyAddr - Address of a key that currently owns the chain tree.
* @param {string[]} newOwnerKeys - List of key addresses for the new owners.
*
* @return {Promise<SetOwnerResponse, RpcError>}
*/
setOwner(chainId, keyAddr, newOwnerKeys) {
var transaction = setOwnershipTransaction(newOwnerKeys);
return this.playTransactions(chainId, keyAddr, [transaction]);
}
/**
* @typedef {Object} SetOwnerResponse
* @property {string} tip - The chain tree tip after the transaction.
*/
/**
* Establish a new token type associated with a chain tree.
*
* @param {string} chainId - The ID of the chain tree.
* @param {string} keyAddr - Address of a key that currently owns the chain tree.
* @param {string} tokenName - Name of the new token
* @param {number} maximum - Maximum number of tokens of this type that can exist
*
* @return {Promise<EstablishTokenResponse, RpcError>}
*/
establishToken(chainId, keyAddr, tokenName, maximum) {
var transaction = establishTokenTransaction(tokenName, maximum);
return this.playTransactions(chainId, keyAddr, [transaction]);
}
/**
* @typedef {Object} EstablishTokenResponse
* @property {string} tip - The chain tree tip after the transaction.
*/
/**
* Mint new tokens of an already established token type associated with a chain
* tree.
*
* @param {string} chainId - The ID of the chain tree.
* @param {string} keyAddr - Address of a key that currently owns the chain tree.
* @param {string} tokenName - Name of the token type
* @param {number} amount - Number of tokens to mint.
*
* @return {Promise<MintTokenResponse, RpcError>}
*/
mintToken(chainId, keyAddr, tokenName, amount) {
var transaction = mintTokenTransaction(tokenName, amount);
return this.playTransactions(chainId, keyAddr, [transaction]);
}
/**
* @typedef {Object} MintTokenResponse
* @property {string} tip - The chain tree tip after the transaction.
*/
/**
* Send an amount of minted tokens to another chain tree.
*
* @param {string} chainId - The ID of the sending chain tree.
* @param {string} keyAddr - Address of a key that currently owns the chain tree.
* @param {string} tokenName - Name of the token type.
* @param {string} destinationChainId - The ID of the recipient chain tree.
* @param {number} amount - Number of tokens to send.
*/
sendToken(chainId, keyAddr, tokenName, destinationChainId, amount) {
return promiseAroundRpcCallback((clbk) => {
this.rpc.sendToken({
creds: this.walletCreds,
chainId: chainId,
keyAddr: keyAddr,
tokenName: tokenName,
destinationChainId: destinationChainId,
amount: amount,
}, clbk);
});
}
/**
* Receive a send token payload from another chain tree.
*
* @param {string} chainId - The ID of the sending chain tree.
* @param {string} keyAddr - Address of a key that currently owns the chain tree.
* @param {string} tokenPayload - The base64-encoded token send payload.
*/
receiveToken(chainId, keyAddr, tokenPayload) {
return promiseAroundRpcCallback((clbk) => {
this.rpc.receiveToken({
creds: this.walletCreds,
chainId: chainId,
keyAddr: keyAddr,
tokenPayload: tokenPayload,
}, clbk);
});
}
/**
* @typedef {Object} StorageAdapterConfigForBadger
* @property {string} path - Path for badger db
*/
/**
* @typedef {Object} StorageAdapterConfigForIpld
* @property {string} [path] - Path to ipld initialized configuration. Either path or address is required.
* @property {string} [address] - Multiaddr for ipfs http api. Either path or address is required.
* @property {boolean} [online=false] - if true, starts IPFS node in online mode
*/
/**
* @typedef {Object} StorageAdapterConfig
* @property {StorageAdapterConfigForBadger} badger - Configure and use the badger storage adapter
* @property {StorageAdapterConfigForIpld} ipld - Configure and use the ipld storage adapter
*/
/**
* @typedef {Object} SerializedChainTree
* @property {string} tip - Hash of the latest root tree node.
* @property {string[]} dag - base58 encoded string array representing the chain tree dag nodes
* @property {Map<String, SerializedSignature>} signatures - Map of serialized signatures
*/
/**
* @typedef {Object} SerializedSignature
* @property {boolean[]} signers - Array indicating which signers have signed
* @property {string} signature - base58 encoded signature
* @property {string} type - Signature type
*/
/**
* @typedef {Object} RpcError - gRPC error object
* @property {number} code - A [gRPC status](https://grpc.io/grpc/node/grpc.html#.status)
* @property {string} details - Details about the error
* @property {?Object} metadata - Additional information about the error
*/
}
exports.setOwnershipPayload = setOwnershipPayload;
exports.setOwnershipTransaction = setOwnershipTransaction;
exports.setDataPayload = setDataPayload;
exports.setDataTransaction = setDataTransaction;
exports.establishTokenPayload = establishTokenPayload;
exports.establishTokenTransaction = establishTokenTransaction;
exports.mintTokenPayload = mintTokenPayload;
exports.mintTokenTransaction = mintTokenTransaction;
exports.connect = connect;
exports.TupeloClient = TupeloClient;