Skip to content

Instantly share code, notes, and snippets.

@christroutner
Forked from pesimista/avax-colaborative-tx.js
Created August 23, 2021 18:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save christroutner/ac8810146ee3664c4ee8d6cb8bd66afe to your computer and use it in GitHub Desktop.
Save christroutner/ac8810146ee3664c4ee8d6cb8bd66afe 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()
@christroutner
Copy link
Author

From MezzMar

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.

@christroutner
Copy link
Author

8/23/21 CT: My next steps are study this code by executing each line and analyzing the objects. I will then be able to compare this code to the collaborative transaction examples for BCH and see if the two use cases in those examples translate to AVAX.

Once that is complete, I can move forward with plans to create a trustless, atomic DEX for the AVAX x-chain, as well as an escrow system for buying and selling general goods and services.

@christroutner
Copy link
Author

The code above was a result of this task:

This is a research task. The deliverable is an example script that shows how to build a collaborative transaction.

A collaborative transaction is one where two or more parties collaborate to build the transaction, by passing partially signed transaction. Here is an example:

Alice and Bob want to trade an (ANT) token for some AVAX. Alice has the token, Bob has the AVAX.
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
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.
Alice checks the transaction, before she adds her signature to her input, and then broadcasts the transaction.

In the above example, there are two key pairs, which represent Alice and Bob. They collaborate to form the transaction by passing the transaction data (encoded as hex) back and forth.

The goal of this task is to see if such a transaction can be created. I'm not sure if it can be. You may need to reach out to Gabriel Cardona and the other Avalanche devs for help on their Discord channel. Give me frequent updates on your progress, so that I can help you hunt for the answers.

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