Created
August 23, 2021 08:28
-
-
Save pesimista/03084a3bf35d35380fe9b891ae00c254 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.