archethic
Version:
Archethic Javascript SDK
396 lines (336 loc) • 11.5 kB
JavaScript
import { deriveKeyPair, deriveAddress, sign } from './crypto.js'
import {
uint8ArrayToHex,
concatUint8Arrays,
encodeInt32,
encodeInt64,
toByteArray,
maybeHexToUint8Array,
maybeStringToUint8Array
} from './utils.js'
const version = 1
const txTypes = {
//User based transaction types
"keychain_access": 254,
"keychain": 255,
"transfer": 253,
"hosting": 252,
"token": 251,
"data": 250,
"contract": 249,
//Network based transaction types
"code_proposal": 5,
"code_approval": 6
}
export default class TransactionBuilder {
/**
* Create a new instance of the transaction builder
*/
constructor() {
this.version = version
this.type = "transfer"
this.data = {
content: new Uint8Array(),
code: new Uint8Array(),
ownerships: [],
ledger: {
uco: {
transfers: []
},
token: {
transfers: []
}
},
recipients: []
}
}
/**
* Set the type of the transaction
* @param {String} type Transaction type
*/
setType(type) {
if (!Object.keys(txTypes).includes(type)) {
throw "Transaction type must be in " + Object.keys(txTypes).map(t => `'${t}'`).join(", ")
}
this.type = type
return this
}
/**
* Add smart contract code to the transcation
* @param {String} code Smart contract code
*/
setCode(code) {
if (typeof (code) !== "string") {
throw "'code' must be a string"
}
this.data.code = new TextEncoder().encode(code)
return this
}
/**
* Add a content to the transaction
* @param {String | Uint8Array} content Hosted content
*/
setContent(content) {
if (typeof (content) !== "string" && !(content instanceof Uint8Array)) {
throw "'content' must be a string or Uint8Array"
}
if (typeof (content) == "string") {
content = new TextEncoder().encode(content)
}
this.data.content = content;
return this
}
/**
* Add an ownership with a secret and its authorized public keys
* @param {String | Uint8Array} secret Secret encrypted (hexadecimal or binary buffer)
* @param {Array} authorizedKeys List of authorized keys
*/
addOwnership(secret, authorizedKeys) {
secret = maybeStringToUint8Array(secret)
if (typeof (authorizedKeys) !== "object" && !(authorizedKeys instanceof Array)) {
throw "'authorizedKeys must be an array"
}
const filteredAuthorizedKeys = []
// Remove duplicated public key
authorizedKeys.reduce((acc, { publicKey, encryptedSecretKey }) => {
publicKey = maybeHexToUint8Array(publicKey)
encryptedSecretKey = maybeHexToUint8Array(encryptedSecretKey)
if (acc[publicKey]) return acc
filteredAuthorizedKeys.push({ publicKey, encryptedSecretKey })
acc[publicKey] = encryptedSecretKey
return acc
}, {})
this.data.ownerships.push({
secret: secret,
authorizedKeys: filteredAuthorizedKeys
})
return this
}
/**
* Add a UCO transfer to the transaction
* @param {String | Uint8Array} to Address of the recipient (hexadecimal or binary buffer)
* @param {Integrer} amount Amount of UCO to transfer (in bigint)
*/
addUCOTransfer(to, amount) {
to = maybeHexToUint8Array(to)
if (isNaN(amount) || amount <= 0) {
throw 'UCO transfer amount must be a positive number'
}
this.data.ledger.uco.transfers.push({ to, amount })
return this
}
/**
* Add a token transfer to the transaction
* @param {String | Uint8Array} to Address of the recipient (hexadecimal or binary buffer)
* @param {Integer} amount Amount of UCO to transfer (in bigint)
* @param {String | Uint8Array} tokenAddress Address of token to spend (hexadecimal or binary buffer)
* @param {Integer} tokenId ID of the token to use (default to 0)
*/
addTokenTransfer(to, amount, tokenAddress, tokenId = 0) {
to = maybeHexToUint8Array(to)
tokenAddress = maybeHexToUint8Array(tokenAddress)
if (isNaN(amount) || amount <= 0) {
throw 'Token transfer amount must be a positive number'
}
if (isNaN(tokenId) || tokenId < 0) {
throw "'tokenId' must be a valid integer >= 0"
}
this.data.ledger.token.transfers.push({
to: to,
amount: amount,
token: tokenAddress,
tokenId: tokenId
})
return this
}
/**
* Add recipient to the transaction
* @param {String | Uint8Array} to Recipient address (hexadecimal or binary buffer)
*/
addRecipient(to) {
to = maybeHexToUint8Array(to)
this.data.recipients.push(to)
return this
}
/**
* Set the transaction builder with Previous Publickey and Previous Signature
* @param {String | Uint8Array} to Previous Signature (hexadecimal)
* @param {String | Uint8Array} to Previous PublicKey (hexadecimal)
*/
setPreviousSignatureAndPreviousPublicKey(prevSign, prevPubKey) {
prevSign = maybeHexToUint8Array(prevSign)
prevPubKey = maybeHexToUint8Array(prevPubKey)
this.previousPublicKey = prevPubKey;
this.previousSignature = prevSign;
return this
}
/**
* Set the transaction builder with address (required for originSign)
* @param {String | Uint8Array} to Address (hexadecimal | Uint8Array)
*
*/
setAddress(addr) {
addr = maybeHexToUint8Array(addr)
this.address = addr;
return this
}
/**
* Generate the transaction address, keys and signatures
* @param {String | Uint8Array} seed Transaction chain seed (hexadecimal or binary buffer)
* @param {Integer} index Number of transaction on the chain
* @param {String} curve Elliptic curve to use for the key generation
* @param {String} hashAlgo Hash algorithm to use for the address generation
*/
build(seed, index, curve, hashAlgo) {
const keypair = deriveKeyPair(seed, index, curve);
const address = deriveAddress(seed, index + 1, curve, hashAlgo)
this.address = address
this.previousPublicKey = keypair.publicKey
const payloadForPreviousSignature = this.previousSignaturePayload()
this.previousSignature = sign(payloadForPreviousSignature, keypair.privateKey)
return this
}
/**
* Sign the transaction with an origin private key
* @param {String | Uint8Array} originPv Origin Private Key (hexadecimal or binary buffer)
*/
originSign(privateKey) {
privateKey = maybeHexToUint8Array(privateKey)
this.originSignature = sign(this.originSignaturePayload(), privateKey)
return this
}
/**
* Set the Txn's originSignature, method called from hardware_libs
* @param {String | Uint8Array} to Signature (hexadecimal)
*/
setOriginSign(signature) {
signature = maybeHexToUint8Array(signature)
this.originSignature = signature
return this
}
/**
* Convert the transaction in JSON
*/
toJSON() {
return JSON.stringify({
version: this.version,
address: uint8ArrayToHex(this.address),
type: this.type,
data: {
content: uint8ArrayToHex(this.data.content),
code: new TextDecoder().decode(this.data.code),
ownerships: this.data.ownerships.map(({ secret, authorizedKeys }) => {
return {
secret: uint8ArrayToHex(secret),
authorizedKeys: authorizedKeys.map(({ publicKey, encryptedSecretKey }) => {
return {
publicKey: uint8ArrayToHex(publicKey),
encryptedSecretKey: uint8ArrayToHex(encryptedSecretKey)
}
})
}
}),
ledger: {
uco: {
transfers: this.data.ledger.uco.transfers.map((t) => {
return {
to: uint8ArrayToHex(t.to),
amount: t.amount
}
})
},
token: {
transfers: this.data.ledger.token.transfers.map((t) => {
return {
to: uint8ArrayToHex(t.to),
amount: t.amount,
tokenAddress: uint8ArrayToHex(t.token),
tokenId: t.tokenId
}
})
}
},
recipients: this.data.recipients.map(uint8ArrayToHex)
},
previousPublicKey: uint8ArrayToHex(this.previousPublicKey),
previousSignature: uint8ArrayToHex(this.previousSignature),
originSignature: this.originSignature && uint8ArrayToHex(this.originSignature)
})
}
/**
* Generate the payload for the previous signature by encoding address, type and data
*/
previousSignaturePayload() {
const bufCodeSize = encodeInt32(this.data.code.length)
let contentSize = this.data.content.length
if (this.data.content instanceof ArrayBuffer) {
contentSize = this.data.content.byteLength
}
const bufContentSize = encodeInt32(contentSize)
const ownershipsBuffer = this.data.ownerships.map(({ secret, authorizedKeys }) => {
const bufAuthKeyLength = Uint8Array.from(toByteArray(authorizedKeys.length))
const authorizedKeysBuffer = [Uint8Array.from([bufAuthKeyLength.length]), bufAuthKeyLength]
// Sort authorized public key by alphabethic order
authorizedKeys.sort((a, b) => uint8ArrayToHex(a.publicKey).localeCompare(uint8ArrayToHex(b.publicKey)))
authorizedKeys.forEach(({ publicKey, encryptedSecretKey }) => {
authorizedKeysBuffer.push(publicKey)
authorizedKeysBuffer.push(encryptedSecretKey)
})
return concatUint8Arrays([
encodeInt32(secret.byteLength),
secret,
concatUint8Arrays(authorizedKeysBuffer)
])
})
const ucoTransfersBuffers = this.data.ledger.uco.transfers.map(function(transfer) {
return concatUint8Arrays([
transfer.to,
encodeInt64(transfer.amount)
])
})
const tokenTransfersBuffers = this.data.ledger.token.transfers.map(function(transfer) {
const bufTokenId = Uint8Array.from(toByteArray(transfer.tokenId))
return concatUint8Arrays([
transfer.token,
transfer.to,
encodeInt64(transfer.amount),
Uint8Array.from([bufTokenId.length]),
bufTokenId
])
})
const bufOwnershipLength = Uint8Array.from(toByteArray(this.data.ownerships.length))
const bufUCOTransferLength = Uint8Array.from(toByteArray(this.data.ledger.uco.transfers.length))
const bufTokenTransferLength = Uint8Array.from(toByteArray(this.data.ledger.token.transfers.length))
const bufRecipientLength = Uint8Array.from(toByteArray(this.data.recipients.length))
return concatUint8Arrays([
encodeInt32(version),
this.address,
Uint8Array.from([txTypes[this.type]]),
bufCodeSize,
this.data.code,
bufContentSize,
this.data.content,
Uint8Array.from([bufOwnershipLength.length]),
bufOwnershipLength,
concatUint8Arrays(ownershipsBuffer),
Uint8Array.from([bufUCOTransferLength.length]),
bufUCOTransferLength,
concatUint8Arrays(ucoTransfersBuffers),
Uint8Array.from([bufTokenTransferLength.length]),
bufTokenTransferLength,
concatUint8Arrays(tokenTransfersBuffers),
Uint8Array.from([bufRecipientLength.length]),
bufRecipientLength,
concatUint8Arrays(this.data.recipients)
])
}
originSignaturePayload() {
const payloadForPreviousSignature = this.previousSignaturePayload()
return concatUint8Arrays([
payloadForPreviousSignature,
this.previousPublicKey,
Uint8Array.from([this.previousSignature.length]),
this.previousSignature,
])
}
}