How do you make Atomic Transfer of two different assets?

I tried to make atomic swaps of two different assets but i keep getting this error "transaction Group: [0] has zero Group but was submitted in a group of 2’ ", even though the transaction has a group ID assigned. Any ideas ?

Hi, @xavyer! Is there any source code you could please link or are there demo snippets you could show to help debug?

You have the situation exactly right: for some reason the node thinks the first transaction in the group has no group ID assigned. In my own hacking and demos, I’ve had the problem before where I mistakenly only added the group ID to some transactions in the group, rather than all of them - maybe it’s that? You say it does have a group assigned though :thinking: once I accidentally wrote a helper for signing transactions that also mutated the transactions, clearing some fields, maybe it’s something like that?

I hope this helps!

Hi Evan. Thank you for your response.
I’ve added the source code below.

const algosdk = require(“algosdk”);
const { mnemonic1, mnemonic2, mnemonic3 } = require("…/vars");

const baseServer = “https://testnet-algorand.api.purestake.io/ps1”;
const port = “”;
const token = {
“X-API-Key”: “”
};
const post_txn_token = {
“X-API-Key”: “”,
“content-type”: “application/x-binary”
};

const algodclient = new algosdk.Algod(token, baseServer, port);
const post_algodclient = new algosdk.Algod(post_txn_token, baseServer, port);

const account1 = algosdk.mnemonicToSecretKey(mnemonic1);
const account2 = algosdk.mnemonicToSecretKey(mnemonic2);
const account3 = algosdk.mnemonicToSecretKey(mnemonic3);

// Structure for changing blockchain params
const cp = {
fee: 0,
firstRound: 0,
lastRound: 0,
genID: “”,
genHash: “”
};
//Utility function to update params from blockchain
const getChangingParms = async function(algodclient) {
let params = await algodclient.getTransactionParams();
cp.firstRound = params.lastRound;
cp.lastRound = cp.firstRound + parseInt(1000);
let sfee = await algodclient.suggestedFee();
cp.fee = sfee.fee;
cp.genID = params.genesisID;
cp.genHash = params.genesishashb64;
};
// Function used to wait for a tx confirmation
const waitForConfirmation = async function(algodclient, txId) {
while (true) {
b3 = await algodclient.pendingTransactionInformation(txId);
if (b3.round != null && b3.round > 0) {
//Got the completed Transaction
console.log("Transaction " + b3.tx + " confirmed in round " + b3.round);
break;
}
}
};

// Asset Creation:
async function createAsset(
note = undefined,
creator,
defaultFrozen = false,
totalIssuance = 100,
unitName = “”,
assetName = “”,
assetURL = “http://localhost”,
assetMetadataHash = “174232y32y”,
decimals = 0
) {
await getChangingParms(algodclient);

// create the asset
let addr = (manager = reserve = freeze = clawback = creator.addr);

// signing and sending “txn” allows “addr” to create an asset
let txn = algosdk.makeAssetCreateTxn(
addr,
cp.fee,
cp.firstRound,
cp.lastRound,
note,
cp.genHash,
cp.genID,
totalIssuance,
decimals,
defaultFrozen,
manager,
reserve,
freeze,
clawback,
unitName,
assetName,
assetURL,
assetMetadataHash
);

let rawSignedTxn = txn.signTxn(creator.sk);
let tx = await post_algodclient.sendRawTransaction(rawSignedTxn);
console.log("Transaction : " + tx.txId);

// wait for transaction to be confirmed and get the assetid
await waitForConfirmation(algodclient, tx.txId);
let ptx = await algodclient.pendingTransactionInformation(tx.txId);
console.log(ptx.txresults.createdasset);
return ptx.txresults.createdasset;
}

async function getAssetInfo(assetID) {
//Get the asset information for the newly changed asset
let assetInfo = await algodclient.assetInformation(assetID);
console.log(“Asset Info”, assetInfo);
return assetInfo;
}

async function getAccountInfo(addr) {
// the new asset listed in the account information
act = await algodclient.accountInformation(addr);
console.log("Account Information for: " + JSON.stringify(act.assets));
}

async function optInForAsset(account, assetID) {
// Opting in to an Asset:
// Transaction from and sender must be the same
const sender = account.addr;
const recipient = sender;
const revocationTarget = undefined;
const closeRemainderTo = undefined;
const note = undefined;
// We are sending 0 of new assets
amount = 0;

// update changing transaction parameters
await getChangingParms(algodclient);

// signing and sending “txn” allows sender to begin accepting asset specified by assetid
const opttxn = algosdk.makeAssetTransferTxn(
sender,
recipient,
closeRemainderTo,
revocationTarget,
cp.fee,
amount,
cp.firstRound,
cp.lastRound,
note,
cp.genHash,
cp.genID,
assetID
);

// Must be signed by the account wishing to opt in to the asset
const rawSignedTxn = opttxn.signTxn(account.sk);
const opttx = await post_algodclient.sendRawTransaction(rawSignedTxn);
console.log("Transaction : " + opttx.txId);
// wait for transaction to be confirmed
await waitForConfirmation(algodclient, opttx.txId);
}

//submit the transaction
(async () => {
const asset1ID = await createAsset(
undefined,
account1,
false,
100,
“Xs”,
“Xavions”,
http://localhost”,
“16efaa3924a6fd9d3a4824799a4ac65d”,
0
);

const asset2ID = await createAsset(
undefined,
account2,
false,
100,
“Ps”,
“Pracoin”,
http://localhost”,
“16efaa3924a6fd9d3a4824799a4ac65d”,
0
);

const asset1Info = await getAssetInfo(asset1ID);
const asset2Info = await getAssetInfo(asset2ID);

await getAccountInfo(account1.addr);
await getAccountInfo(account2.addr);

await optInForAsset(account2, asset1ID);
await optInForAsset(account1, asset2ID);

const revocationTarget = undefined;
const closeRemainderTo = undefined;
const note = undefined;

// update changing transaction parameters
await getChangingParms(algodclient);

// signing and sending “txn” will send “amount” assets from “sender” to “recipient”
let xtxn = algosdk.makeAssetTransferTxn(
account1.addr,
account2.addr,
closeRemainderTo,
revocationTarget,
cp.fee,
20,
cp.firstRound,
cp.lastRound,
note,
cp.genHash,
cp.genID,
asset1ID
);

// signing and sending “txn” will send “amount” assets from “sender” to “recipient”
let xtxn1 = algosdk.makeAssetTransferTxn(
account2.addr,
account1.addr,
closeRemainderTo,
revocationTarget,
cp.fee,
20,
cp.firstRound,
cp.lastRound,
note,
cp.genHash,
cp.genID,
asset2ID
);

// Store both transactions
let txns = [xtxn, xtxn1];

// Group both transactions
let txgroup = algosdk.assignGroupID(txns);

// Sign each transaction in the group with
// correct key
let signed =
signed.push( xtxn.signTxn( account1.sk ) )
signed.push( xtxn1.signTxn( account2.sk ) )

let tx = (await post_algodclient.sendRawTransactions(signed));
console.log("Transaction : " + tx.txId);

// Wait for transaction to be confirmed
await waitForConfirmation(algodclient, tx.txId)

await getAccountInfo(account1.addr);
await getAccountInfo(account2.addr);
})().catch(e => {
console.log(e);
console.trace();
});

Hello @xavyer,
I wrote a TEAL program to handle atomic swaps of two ASA. Included in the forum post are instructions and the template. I’d be happy to receive feedback if it addresses your needs.

1 Like

I think the confusion might be here. assignGroupID doesn’t add the groupID in-place, it returns a new array. In other words, xtxn and xtxn1 still have no group ID after the assignGroupID call, instead txgroup will be a copy of txns with the group IDs added. So, I think you want txgroup[0].signTxn(account1.sk) and txgroup[1].signTxn(account1.sk).

@Evan I get same error for this too

@ryanRfox Thank you for your response. I will look into it and let you know.

Can you log or otherwise inspect the txgroup members? I wonder what they look like :thinking:

@Evan Please refer to the video attached below

I got this to work locally on my node by just connecting to it and running your example. It also worked with PureStake connection. Have you updated the algosdk?

@JasonW That was the problem. It works fine after I updated the sdk. Thank you for your time.

1 Like

@JasonW @Evan When I am trying the atomic transaction in another way. It’s now throwing me again the same error:

TransactionPool.Remember: transactionGroup: [0] had zero Group but was submitted in a group of 2

  const sampAsset = {
  from: account1.addr,
  to: account2.addr,
  fee: params.minFee,
  flatFee: true,
  amount: 2,
  firstRound: params.lastRound,
  lastRound: params.lastRound + 1000,
  genesisID: params.genesisID,
  genesisHash: params.genesishashb64,
  assetIndex: 315272,
  type: "axfer"
};

const sampPayment = {
  from: account2.addr,
  to: account1.addr,
  fee: params.minFee,
  flatFee: true,
  amount: 10,
  firstRound: params.lastRound,
  lastRound: params.lastRound + 1000,
  genesisID: params.genesisID,
  genesisHash: params.genesishashb64
};

const txn = [sampAsset, sampPayment];
const txgroup = algosdk.assignGroupID(txn);
const signed = [];
const sampAssetTx = algosdk.signTransaction(txgroup[0], account1.sk);
signed.push(sampAssetTx.blob);
const sampPaymentTx = algosdk.signTransaction(txgroup[1], account2.sk);
signed.push(sampPaymentTx.blob);

const tx = await post_algodclient.sendRawTransactions(signed);

Any ideas?

I think there is an issue with algosdk.signTransaction when used in a grouped tx. Can you not use the first way?

The signTx would only applicable for the transaction objects generated using makeAssetTxn or makePaymentTx. We cannot encode the object (algosdk.encodeObj) from makeAssetTxn or makePaymentTx. So that’s why I am creating the transaction as normal object, trying to encode the normal object and sign it .

Why I am using algosdk.encodeObj is try to split the transaction, sign it and combine it later and then send it to algo blockchain.

@JasonW I fixed the issue, Now it’s working fine. I added the group as a parameter in Transaction file(transaction.js) in sdk.

It’s occuring due to whenever the algosdk.signTransaction called it’s creating new transaction object from normal object. Since the group parameter not in the Transaction constructor object, group gets removed from the object. That’s why the issue is happening.

I attached the lines I modified here. I hope it get’s fixed soon in the algorand-sdk.

1 Like

Can you create a pr for this on the sdk? What error do you get when you try to encodeObj using the tx returned from the maker commands?

If I run the encodeobj from maker commands. It is returning me the below error.

  let xtxn1 = algosdk.makeAssetTransferTxn(
  account1.addr,
  account2.addr,
  closeRemainderTo,
  revocationTarget,
  cp.fee,
  20,
  cp.firstRound,
  cp.lastRound,
  note,
  cp.genHash,
  cp.genID,
  assetID
 );
 const encodedTransaction = algosdk.encodeObj(xtxn1);

The object contains empty or 0 values

Can you try transaction.get_obj_for_encoding() and then encodeObj the result?

I actually tried that too. But after you decoded the object from the file we have two problems:

  1. You cannot convert that decoded object back to transaction object. (we need to have an helper function from algosdk regarding this).
  2. You cannot sign(algosdk.signTransaction) the decoded object it will throw “genesis hash must be specified and in a base64 string.”