Skip to content

Commit

Permalink
Merge pull request #80 from Bitcoin-com/stage
Browse files Browse the repository at this point in the history
fix(tokenUtxoDetails): support mint token transactions
  • Loading branch information
christroutner authored Dec 16, 2019
2 parents 06280da + 7756916 commit 8b267af
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 18 deletions.
4 changes: 4 additions & 0 deletions lib/TokenType1.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ var TokenType1 = /** @class */ (function () {
});
}
inputUtxos = balances_1.slpTokenUtxos[tokenId];
// console.log(`inputUtxos: ${JSON.stringify(inputUtxos, null, 2)}`)
// console.log(`balances.nonSlpUtxos: ${JSON.stringify(balances.nonSlpUtxos, null, 2)}`)
if (inputUtxos === undefined)
throw new Error("Could not find any SLP token UTXOs");
inputUtxos = inputUtxos.concat(balances_1.nonSlpUtxos);
inputUtxos.forEach(function (txo) { return (txo.wif = sendConfig.fundingWif); });
return [4 /*yield*/, bitboxNetwork.simpleTokenSend(tokenId, amount_1, inputUtxos, sendConfig.tokenReceiverAddress, bchChangeReceiverAddress)];
Expand Down
62 changes: 46 additions & 16 deletions lib/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,19 @@ var Util = /** @class */ (function (_super) {
outObj.transactionType = "mint";
outObj.tokenId = script[4];
mintBatonVout = 0;
// Dev note: Haven't seen this if-statement in the wild. Copied from
// Badger Wallet code.
if (typeof script[5] === "string" && script[5].startsWith("OP_"))
mintBatonVout = parseInt(script[5].slice(3));
// This is the common use case with slp-sdk examples.
else
mintBatonVout = parseInt(script[5]);
outObj.mintBatonVout = mintBatonVout;
// Check if baton was passed or destroyed.
// Dev Note: There should be some more extensive checking here. The most
// common way of 'burning' the minting baton is to set script[5] to a
// value of 0, but it could also point to a non-existant vout.
// TODO: Add checking if script[5] refers to a non-existant vout.
outObj.batonStillExists = false; // false by default.
if (mintBatonVout > 1)
outObj.batonStillExists = true;
Expand Down Expand Up @@ -567,13 +576,13 @@ var Util = /** @class */ (function (_super) {
// Extract the boolean result
validations = validations.map(function (x) { return x.valid; });
_loop_2 = function (i) {
var thisValidation, thisUtxo, slpData, voutMatch, genesisData;
var thisValidation, thisUtxo, slpData, genesisData, voutMatch, genesisData;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
thisValidation = validations[i];
thisUtxo = utxos[i];
if (!thisValidation) return [3 /*break*/, 4];
if (!thisValidation) return [3 /*break*/, 7];
return [4 /*yield*/, this_2.decodeOpReturn(thisUtxo.txid)
// console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`)
// Handle Genesis SLP transactions.
Expand Down Expand Up @@ -602,26 +611,47 @@ var Util = /** @class */ (function (_super) {
validations[i] = thisUtxo;
}
}
// Handle Mint SLP transactions.
if (slpData.transactionType === "mint") {
if (thisUtxo.vout !== slpData.mintBatonVout && // UTXO is not a mint baton output.
thisUtxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens.
)
// Can safely be marked as false.
validations[i] = false;
}
if (!(slpData.transactionType === "send")) return [3 /*break*/, 4];
voutMatch = slpData.spendData.filter(function (x) { return thisUtxo.vout === x.vout; });
if (!(voutMatch.length === 0)) return [3 /*break*/, 2];
if (!(slpData.transactionType === "mint")) return [3 /*break*/, 4];
if (!(thisUtxo.vout !== slpData.mintBatonVout && // UTXO is not a mint baton output.
thisUtxo.vout !== 1) // UTXO is not the reciever of the genesis or mint tokens.
) return [3 /*break*/, 2]; // UTXO is not the reciever of the genesis or mint tokens.
// Can safely be marked as false.
validations[i] = false;
return [3 /*break*/, 4];
case 2: return [4 /*yield*/, this_2.decodeOpReturn(slpData.tokenId)
// Hydrate the UTXO object with information about the SLP token.
];
case 3:
genesisData = _a.sent();
// Hydrate the UTXO object with information about the SLP token.
thisUtxo.utxoType = "token";
thisUtxo.transactionType = "mint";
thisUtxo.tokenId = slpData.tokenId;
thisUtxo.tokenTicker = genesisData.ticker;
thisUtxo.tokenName = genesisData.name;
thisUtxo.tokenDocumentUrl = genesisData.documentUrl;
thisUtxo.tokenDocumentHash = genesisData.documentHash;
thisUtxo.decimals = genesisData.decimals;
thisUtxo.mintBatonVout = slpData.mintBatonVout;
thisUtxo.batonStillExists = slpData.batonStillExists;
// Calculate the real token quantity.
thisUtxo.tokenQty =
slpData.quantity / Math.pow(10, thisUtxo.decimals);
validations[i] = thisUtxo;
_a.label = 4;
case 4:
if (!(slpData.transactionType === "send")) return [3 /*break*/, 7];
voutMatch = slpData.spendData.filter(function (x) { return thisUtxo.vout === x.vout; });
if (!(voutMatch.length === 0)) return [3 /*break*/, 5];
validations[i] = false;
return [3 /*break*/, 7];
case 5: return [4 /*yield*/, this_2.decodeOpReturn(slpData.tokenId)
// console.log(
// `genesisData: ${JSON.stringify(genesisData, null, 2)}`
// )
// Hydrate the UTXO object with information about the SLP token.
];
case 3:
case 6:
genesisData = _a.sent();
// console.log(
// `genesisData: ${JSON.stringify(genesisData, null, 2)}`
Expand All @@ -638,8 +668,8 @@ var Util = /** @class */ (function (_super) {
thisUtxo.tokenQty =
voutMatch[0].quantity / Math.pow(10, thisUtxo.decimals);
validations[i] = thisUtxo;
_a.label = 4;
case 4: return [2 /*return*/];
_a.label = 7;
case 7: return [2 /*return*/];
}
});
};
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"build": "node ./node_modules/gulp/bin/gulp.js build && ./node_modules/typescript/bin/tsc",
"test": "TEST=unit nyc --reporter=text mocha --require babel-core/register --timeout 10000 test/ --exit",
"test:unit": "TEST=unit nyc --reporter=text mocha --require babel-core/register --timeout 10000 test/ --exit",
"test:integration": "TEST=integration nyc --reporter=text mocha --require babel-core/register --timeout 10000 test/ --exit",
"test:integration": "TEST=integration nyc --reporter=text mocha --require babel-core/register --timeout 30000 test/ --exit",
"test:e2e": "mocha --timeout 60000 test/e2e --exit",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"coverage:report": "nyc --reporter=html mocha --require babel-core/register --timeout 10000",
"semantic-release": "semantic-release"
Expand Down
4 changes: 4 additions & 0 deletions src/TokenType1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ class TokenType1 {
}

let inputUtxos = balances.slpTokenUtxos[tokenId]
// console.log(`inputUtxos: ${JSON.stringify(inputUtxos, null, 2)}`)
// console.log(`balances.nonSlpUtxos: ${JSON.stringify(balances.nonSlpUtxos, null, 2)}`)

if(inputUtxos === undefined) throw new Error(`Could not find any SLP token UTXOs`)

inputUtxos = inputUtxos.concat(balances.nonSlpUtxos)

Expand Down
35 changes: 34 additions & 1 deletion src/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,12 +254,20 @@ class Util extends BITBOXUtil {

// Locate the vout UTXO containing the minting baton.
let mintBatonVout = 0
// Dev note: Haven't seen this if-statement in the wild. Copied from
// Badger Wallet code.
if (typeof script[5] === "string" && script[5].startsWith("OP_"))
mintBatonVout = parseInt(script[5].slice(3))
// This is the common use case with slp-sdk examples.
else mintBatonVout = parseInt(script[5])

outObj.mintBatonVout = mintBatonVout

// Check if baton was passed or destroyed.
// Dev Note: There should be some more extensive checking here. The most
// common way of 'burning' the minting baton is to set script[5] to a
// value of 0, but it could also point to a non-existant vout.
// TODO: Add checking if script[5] refers to a non-existant vout.
outObj.batonStillExists = false // false by default.
if (mintBatonVout > 1) outObj.batonStillExists = true

Expand Down Expand Up @@ -485,9 +493,34 @@ class Util extends BITBOXUtil {
if (
thisUtxo.vout !== slpData.mintBatonVout && // UTXO is not a mint baton output.
thisUtxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens.
)
) {
// Can safely be marked as false.
validations[i] = false

// If UTXO passes validation, then return formatted token data.
} else {
const genesisData = await this.decodeOpReturn(slpData.tokenId)

// Hydrate the UTXO object with information about the SLP token.
thisUtxo.utxoType = "token"
thisUtxo.transactionType = "mint"
thisUtxo.tokenId = slpData.tokenId

thisUtxo.tokenTicker = genesisData.ticker
thisUtxo.tokenName = genesisData.name
thisUtxo.tokenDocumentUrl = genesisData.documentUrl
thisUtxo.tokenDocumentHash = genesisData.documentHash
thisUtxo.decimals = genesisData.decimals

thisUtxo.mintBatonVout = slpData.mintBatonVout
thisUtxo.batonStillExists = slpData.batonStillExists

// Calculate the real token quantity.
thisUtxo.tokenQty =
slpData.quantity / Math.pow(10, thisUtxo.decimals)

validations[i] = thisUtxo
}
}

// Handle Send SLP transactions.
Expand Down
81 changes: 81 additions & 0 deletions test/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -978,5 +978,86 @@ describe("#Utils", () => {
"tokenQty"
])
})

it("should return details for a MINT token utxo", async () => {
// Mock the call to REST API
if (process.env.TEST === "unit") {
// Stub the call to validateTxid
sandbox.stub(SLP.Utils, "validateTxid").resolves([
{
txid:
"cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76",
valid: true
}
])

// Stub the calls to decodeOpReturn.
sandbox
.stub(SLP.Utils, "decodeOpReturn")
.onCall(0)
.resolves({
tokenType: 1,
transactionType: "mint",
tokenId:
"38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0",
mintBatonVout: 2,
batonStillExists: true,
quantity: "1000000000000",
tokensSentTo:
"bitcoincash:qpszr2za8hht020trekw4jc9kar2v7jva5xej5uqns"
})
.onCall(1)
.resolves({
tokenType: 1,
transactionType: "genesis",
ticker: "PSF",
name: "Permissionless Software Foundation",
documentUrl: "psfoundation.cash",
documentHash: "",
decimals: 8,
mintBatonVout: 2,
initialQty: 19882.09163133,
tokensSentTo:
"bitcoincash:qpgeu2kvk4assnj3klez6yv8z2mv6q9vdy48m62vhn",
batonHolder:
"bitcoincash:qpgeu2kvk4assnj3klez6yv8z2mv6q9vdy48m62vhn"
})
}

const utxos = [
{
txid:
"cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76",
vout: 1,
amount: 0.00000546,
satoshis: 546,
height: 600297,
confirmations: 76
}
]

const data = await SLP.Utils.tokenUtxoDetails(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)

assert2.hasAnyKeys(data[0], [
"txid",
"vout",
"amount",
"satoshis",
"height",
"confirmations",
"utxoType",
"transactionType",
"tokenId",
"tokenTicker",
"tokenName",
"tokenDocumentUrl",
"tokenDocumentHash",
"decimals",
"mintBatonVout",
"batonStillExists",
"tokenQty"
])
})
})
})
102 changes: 102 additions & 0 deletions test/e2e/send-token/send-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
This is an end-to-end test which verified the happy-path of sending an SLP
token. It's really a test of the speed of SLPDB to process new token
transactions.
This program expects two wallets. Wallet 1 must have a small amount of BCH
and an inventory of SLP test tokens. Wallet 2 is the reieving wallet.
*/

// Inspect utility used for debugging.
const util = require("util")
util.inspect.defaultOptions = {
showHidden: true,
colors: true,
depth: 1
}

// const SLPSDK = require("../../../lib/SLP")
// const slpsdk = new SLPSDK()

const WALLET1 = `../wallet1.json`
const WALLET2 = `../wallet2.json`

const lib = require("../util/e2e-util")

// The main test function.
// Sends a token and reports on how long it takes to show up in SLPDB production.
async function sendTokenTest() {
try {
// Open the sending wallet.
const sendWallet = await lib.openWallet(WALLET1)
//console.log(`sendWallet: ${JSON.stringify(walletInfo, null, 2)}`)

// Open the recieving wallet.
const recvWallet = await lib.openWallet(WALLET2)
//console.log(`recvWallet: ${JSON.stringify(walletInfo, null, 2)}`)

// Get the balance of the recieving wallet.
// const testTokens = recvWallet.tokenBalance.filter(
// x => testTokenId === x.tokenId
// )
// const startBalance = testTokens[0].balance
const startBalance = await lib.getTestTokenBalance(recvWallet)
console.log(`Starting balance: ${startBalance} test tokens.`)
let newBalance = startBalance

// Send a token to the recieving wallet.
await lib.sendToken(sendWallet, recvWallet)
console.log(`Sent test token.`)

// Track the time until the balance for the recieving wallet has been updated.
const startTime = new Date()
const waitTime = 10000 // time in milliseconds

// Loop with a definite exit point, so we don't loop forever.
for (let i = 0; i < 50; i++) {
await sleep(waitTime) // Wait for a while before checking

console.log(`Checking token balance...`)
newBalance = await lib.getTestTokenBalance(recvWallet)

// Break out of the loop once a new balance is detected.
if (newBalance > startBalance) break

// Provide high-level warnings.
const secondsPassed = (i * waitTime) / 1000
if (secondsPassed > 60 * 10) {
console.log(`More than 10 minutes passed.`)
return false // Fail the test.
} else if (secondsPassed > 60 * 5) {
console.log(`More than 5 minutes passed.`)
} else if (secondsPassed > 60) {
console.log(`More than 1 minute passed.`)
}
}

// Calculate the amount of time that has passed.
const endTime = new Date()
let deltaTime = (endTime.getTime() - startTime.getTime()) / 60000
deltaTime = lib.threeDecimals(deltaTime)
console.log(`SLPDB updated token balance in ${deltaTime} minutes.`)

// Consolidate the SLP UTXOs on the recieve wallet.
// CT 9/11/19: This causes the test to fail if done every time. This will
// have to be done manually.
//await lib.sendToken(recvWallet, recvWallet)

return deltaTime // Return the time in minutes it took for SLPDB to update.
} catch (err) {
console.log(`Error in e2e/send-token.js/sendTokenTest(): `, err)
return false
}
}

// Promise based sleep function.
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}

module.exports = {
sendTokenTest
}
Loading

0 comments on commit 8b267af

Please sign in to comment.