Skip to content

Instantly share code, notes, and snippets.

@pesimista
Created August 23, 2021 08:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pesimista/03084a3bf35d35380fe9b891ae00c254 to your computer and use it in GitHub Desktop.
Save pesimista/03084a3bf35d35380fe9b891ae00c254 to your computer and use it in GitHub Desktop.
const createHash = require('create-hash')
const { Avalanche, BinTools, BN } = require('avalanche')
const { KeyChain } = require('avalanche/dist/apis/evm')
const avm = require('avalanche/dist/apis/avm')
const { Signature } = require('avalanche/dist/common/credentials')
const ava = new Avalanche('api.avax.network', 443, 'https')
const bintools = BinTools.getInstance()
const xchain = ava.XChain()
const aliceWallet = {
address: 'X-avax10ps8jjqmd3s29wuqa7fanpwk9g63yjxdnmawqx',
privateKey: 'PrivateKey-hzkJjZ3vh23cMEX7xbKMVSQuZVsehdRnZxyrz1CYNpbVFvdUv',
utxos: [
{
txid: '2ns8XVRdy8TRVJJaa9BTNTu2AvpdGweQ3vXfq3WnJVzApbXCH2',
outputIdx: '00000001',
amount: 100,
assetID: '2jgTFB6MM4vwLzUNWFYGPfyeQfpLaEqj4XWku6FoW7vaGrrEd5',
typeID: 7
}
]
}
const bobWallet = {
address: 'X-avax1wcjw6t2kqafservk445awwyufjqze29y7j33m9',
privateKey: 'PrivateKey-GkhJmNAkKqH6us3neA7hCESexVzUPCovKCGFjwpaZsj3LTuGA',
utxos: [
{
txid: 'qRTFJsBdBBk5PZatmbXMwKvDGUQAxqLi8jRGXVwqVe8dCqTbW',
outputIdx: '00000000',
amount: 21000000,
assetID: 'FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z',
typeID: 7
}
]
}
const addrReferences = {}
const parseUtxo = (utxoJSON, address) => {
const amount = new BN(utxoJSON.amount)
const tokenTransferInput = new avm.SECPTransferInput(amount)
tokenTransferInput.addSignatureIdx(0, address)
const tokenTxInput = new avm.TransferableInput(
bintools.cb58Decode(utxoJSON.txid),
Buffer.from(utxoJSON.outputIdx, 'hex'),
bintools.cb58Decode(utxoJSON.assetID),
tokenTransferInput
)
return tokenTxInput
}
const generateOutput = (amount, address, assetID) => {
const tokenTransferOutput = new avm.SECPTransferOutput(
amount,
[address]
)
return new avm.TransferableOutput(
assetID,
tokenTransferOutput
)
}
/**
* This method assumes that all the utxos have only one associated address
* @param {avm.UnsignedTx} tx
* @param {KeyChain} keychain
* @param {Credential} credentials
* @param {Object} reference
*/
const partialySignTx = (tx, keychain, credentials = [], reference = {}) => {
const txBuffer = tx.toBuffer()
const msg = Buffer.from(
createHash('sha256').update(txBuffer).digest()
)
const ins = tx.getTransaction().getIns()
for (let i = 0; i < ins.length; i++) {
const input = ins[i]
const cred = avm.SelectCredentialClass(input.getInput().getCredentialID())
const inputid = bintools.cb58Encode(input.getOutputIdx())
try {
const source = xchain.parseAddress(reference[inputid])
const keypair = keychain.getKey(source)
const signval = keypair.sign(msg)
const sig = new Signature()
sig.fromBuffer(signval)
cred.addSignature(sig)
console.log(`Successfully signed input ${i}, ( ${inputid} signed with ${reference[inputid]} )`)
credentials[i] = cred
} catch (error) {
console.log(`Skipping input ${i}: ${error.message}, ( ${inputid})`)
}
}
console.log(' ')
return new avm.Tx(tx, credentials)
}
// Generates the ANT transaction, ready to broadcast to network.
const colaborate = async () => {
try {
const avaxID = await xchain.getAVAXAssetID()
// First side of the transaction - Alice first time
// Alice creates an *unsigned* transaction, which she passes to Bob:
// - 1 input: the token as an input
// - 1 output: 0.1 AVAX to her address
const aliceAddressBuffer = xchain.parseAddress(aliceWallet.address)
const bobAddressBuffer = xchain.parseAddress(bobWallet.address)
let [tokenInput] = aliceWallet.utxos
tokenInput = parseUtxo(tokenInput, aliceAddressBuffer)
const initialInputs = [tokenInput]
const tokenInputId = bintools.cb58Encode(tokenInput.getOutputIdx())
addrReferences[tokenInputId] = aliceWallet.address
// get the desired avax outputs for the transaction
const avaxToReceive = new BN(0.1)
const avaxOutput = generateOutput(avaxToReceive, aliceAddressBuffer, avaxID)
const initialOutputs = [avaxOutput]
// Build the transcation
const partialTx = new avm.BaseTx(
ava.getNetworkID(),
bintools.cb58Decode(xchain.getBlockchainID()),
initialOutputs,
initialInputs,
Buffer.from('from alice')
)
// This is what Alice has to send and what Bob will receive
const hexString = partialTx.toBuffer()
// Second side of the transaction - Bob first time
// Bob adds to the transaction before passing it back to Alice:
// - 1 input of 0.1 AVAX (plus tx fees), which he signs.
// - 1 output of the token, going to his address.
// Parse back the transaction from base58 to an object
const docodedTx = new avm.BaseTx()
docodedTx.fromBuffer(hexString)
const finalInputs = docodedTx.getIns()
const finalOutputs = docodedTx.getOuts()
let [avaxInput] = bobWallet.utxos
avaxInput = parseUtxo(avaxInput, bobAddressBuffer)
finalInputs.push(avaxInput)
const avaxInputId = bintools.cb58Encode(avaxInput.getOutputIdx())
addrReferences[avaxInputId] = bobWallet.address
// get the desired token outputs for the transaction
const tokensToReceive = new BN(tokenInput.amount)
const tokenOutput = generateOutput(tokensToReceive, bobAddressBuffer, avaxID)
finalOutputs.push(tokenOutput)
// Build the partially signed transcation
const wholeTx = new avm.BaseTx(
ava.getNetworkID(),
bintools.cb58Decode(xchain.getBlockchainID()),
finalOutputs,
finalInputs,
Buffer.from('from bob')
)
const unsignedTxBob = new avm.UnsignedTx(wholeTx)
// Sign bob inputs with his keychain
const bobKeyChain = new KeyChain(ava.getHRP(), 'X')
bobKeyChain.importKey(bobWallet.privateKey)
const signedByBob = partialySignTx(unsignedTxBob, bobKeyChain, [], addrReferences)
// Bob sends back the tx with his inputs signed
const signedByBobString = bintools.cb58Encode(signedByBob.toBuffer())
// Finally, Alice checks the transaction, before she adds her signature to her input, and then broadcasts the transaction.
const partiallySigned = new avm.Tx()
partiallySigned.fromBuffer(bintools.cb58Decode(signedByBobString))
// Sign Alice inputs with her keychain, and the previous credentials
const aliceKeyChain = new KeyChain(ava.getHRP(), 'X')
aliceKeyChain.importKey(aliceWallet.privateKey)
const previousCredentials = partiallySigned.getCredentials()
const unsignedTxAlice = partiallySigned.getUnsignedTx()
const signedByAlice = partialySignTx(unsignedTxAlice, aliceKeyChain, previousCredentials, addrReferences)
// this is the fully signed transaction that must be broadcasted
return signedByAlice
} catch (err) {
console.log('Error in send-token.js/sendTokens()')
throw err
}
}
colaborate()
@pesimista
Copy link
Author

pesimista commented Aug 23, 2021

This script is a demonstration on how a collaborative transaction can be performed on the avalanche X Chain, it uses a slightly modified signing method where it doesn't require all the keys to be present at a given time, thus making possible to partially sign a transaction and share it with somebody else.

A small adjustment that needed to be done was adding an address reference object to keep track of which UTXO belongs to which address, since when the transaction is parsed back from code base 58 or hex to a JavaScript object it obfuscates the owner addresses. So whenever an input it's parsed this address reference gets updated. It must be send along with the cb58 string that represents the transaction, so the subsequent parties are able to sign successfully and broadcast the tx at last.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment