Skip to content

Instantly share code, notes, and snippets.

@jasonpaulos
Created January 19, 2022 18:34
Show Gist options
  • Save jasonpaulos/38e9ed474e110a2c34b01310dcdbf24a to your computer and use it in GitHub Desktop.
Save jasonpaulos/38e9ed474e110a2c34b01310dcdbf24a to your computer and use it in GitHub Desktop.
const path = require('path');
const fs = require('fs');
const execSync = require('child_process').execSync;
const tmp = require('tmp');
const sha512 = require('js-sha512');
const algosdk = require('algosdk');
function compileProgram(source) {
const sourceFile = tmp.fileSync();
const outputFile = tmp.fileSync();
fs.writeFileSync(sourceFile.name, source);
execSync(`goal clerk compile ${sourceFile.name} -o ${outputFile.name}`);
const output = fs.readFileSync(outputFile.name);
return output;
}
function randomInt(maximum) {
return Math.floor(Math.random() * maximum);
}
function generateIntPlaceholder() {
const byteLength = 8;
const bytes = [];
for (let i = 0; i < byteLength; i++) {
const randomValue = i === 0 ? 128 + randomInt(128) : randomInt(256);
bytes.push(randomValue);
}
const hex = '0x' + Buffer.from(bytes).toString('hex');
const number = BigInt(hex);
return {
value: number.toString(10),
bytes: encodeVaruint(number),
};
}
function generateAddrPlaceholder() {
const addr = algosdk.generateAccount().addr;
const bytes = algosdk.decodeAddress(addr).publicKey;
return {
value: addr,
bytes,
};
}
function generateBytesPlaceholder(byteLength) {
const bytes = [];
for (let i = 0; i < byteLength; i++) {
const randomValue = i === 0 ? 128 + randomInt(128) : randomInt(256);
bytes.push(randomValue);
}
const buffer = Buffer.from(bytes);
return {
value: '0x' + buffer.toString('hex'),
bytes: new Uint8Array(buffer),
};
}
function generateCompiledContractWithPlaceholders(fileContents) {
const contractTemplate = fileContents.toString('utf8');
const templateVariables = new Set(contractTemplate.match(/(int|byte|addr) TMPL_[A-Z0-9_]+/g) || []);
const pushIntVars = contractTemplate.match(/pushint TMPL_[A-Z0-9_]+/g) || [];
for (const v of pushIntVars) {
templateVariables.add(v.substr('push'.length));
}
const pushBytesVars = contractTemplate.match(/pushbytes TMPL_[A-Z0-9_]+/g) || [];
for (const v of pushBytesVars) {
templateVariables.add(v.replace('pushbytes', 'byte'));
}
for (const [blockType, type] of [['intcblock', 'int'], ['bytecblock', 'byte']]) {
let i = 0;
while (i >= 0) {
i = contractTemplate.indexOf(blockType, i+1);
if (i !== -1) {
const line = contractTemplate.substring(i, contractTemplate.indexOf('\n', i));
const vars = line.match(/TMPL_[A-Z0-9_]+/g) || [];
for (const v of vars) {
templateVariables.add(`${type} ${v}`);
}
}
}
}
if (templateVariables.size === 0) {
throw new Error('Template contract has no variables.');
}
const placeholderVars = [];
for (const variable of templateVariables) {
const type = variable.substring(0, variable.indexOf(' '));
const name = variable.substring(variable.indexOf('_') + 1);
if (type === 'int') {
placeholderVars.push({
name,
type: 'int',
placeholder: generateIntPlaceholder(),
});
} else if (type === 'addr') {
placeholderVars.push({
name,
type: 'addr',
placeholder: generateAddrPlaceholder(),
});
} else if (type === 'byte') {
placeholderVars.push({
name,
type: 'bytes',
placeholder: generateBytesPlaceholder(name === 'ORIGINAL_EXCHANGE' ? 32 : 8), // WARNING: always assuming size is 8 bytes, except for ORIGINAL_EXCHANGE
});
} else {
throw new Error(`Tempate variable ${name} has unknown type: ${type}`);
}
}
// compile the contract with zero values first to make sure our placeholder bytes don't
// accidentally match anything already there
const zeroSource = contractTemplate.replace(/TMPL_[A-Z0-9_]+/g, match => {
for (const variable of placeholderVars) {
if (match !== 'TMPL_' + variable.name) {
continue;
}
if (variable.type === 'int') {
return '0';
}
if (variable.type === 'addr') {
return 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ';
}
return '0x00';
}
throw new Error(`Template variable not found: ${match}`);
});
const compiledZero = compileProgram(zeroSource);
// regenerate placeholders if they are already present in the compiled contract
for (const variable of placeholderVars) {
while (compiledZero.indexOf(variable.placeholder.bytes) !== -1) {
if (variable.type === 'int') {
variable.placeholder = generateIntPlaceholder();
} else if (variable.type === 'addr') {
variable.placeholder = generateAddrPlaceholder();
} else {
variable.placeholder = generateBytesPlaceholder(variable.placeholder.bytes.byteLength);
}
}
}
// compile the contract with our placeholders
const placeholderSource = contractTemplate.replace(/TMPL_[A-Z0-9_]+/g, match => {
for (const variable of placeholderVars) {
if (match !== 'TMPL_' + variable.name) {
continue;
}
return variable.placeholder.value;
}
throw new Error(`Template variable not found: ${match}`);
});
const compiledPlaceholder = compileProgram(placeholderSource);
// ensure that the placeholders appear only once in the compiled contract and record their index
for (const variable of placeholderVars) {
const firstIndex = compiledPlaceholder.indexOf(variable.placeholder.bytes);
if (firstIndex === -1) {
throw new Error(`Placeholder for variable ${variable.name} is not in compiled program.`);
}
const length = variable.placeholder.bytes.byteLength;
const secondIndex = compiledPlaceholder.indexOf(variable.placeholder.bytes, firstIndex + length);
if (secondIndex !== -1) {
throw new Error(`Placeholder for variable ${variable.name} appears multiple times in compiled program.`);
}
variable.placeholder.index = firstIndex;
}
// replace the placeholder values from before with zeros. This is just to make the content of
// the compiled contract deterministic.
for (const variable of placeholderVars) {
for (let i = 0; i < variable.placeholder.bytes.byteLength; i++) {
compiledPlaceholder[variable.placeholder.index + i] = 0;
}
}
return {
variables: placeholderVars,
contract: compiledPlaceholder,
};
}
function prepareFileContents(variables, contract) {
const serializedPlaceholderVars = variables.map(variable => (
` ${JSON.stringify(variable.name)}: {
type: ${JSON.stringify(variable.type)},
index: ${variable.placeholder.index},
length: ${variable.placeholder.bytes.byteLength},
}`));
const serializedContract = `Uint8Array.from(${JSON.stringify(Array.from(contract))})`;
const addressPreimage = Uint8Array.from([...Buffer.from("Program"), ...contract]);
const contractTemplatePK = Uint8Array.from(sha512.sha512_256.array(addressPreimage));
const contractTemplateAddr = algosdk.encodeAddress(contractTemplatePK);
return `const templateVariables = {\n${serializedPlaceholderVars.join(',\n')}\n};\nconst contractTemplateAddr = "${contractTemplateAddr}";\nconst compiledContractTemplate = ${serializedContract};\nexport { templateVariables, contractTemplateAddr, compiledContractTemplate };\n`;
}
function main(args) {
if (args.length < 3) {
console.log('No target file specified.');
return;
}
if (args.length > 3) {
console.log(`Too many arguments. Expected 1 but got ${args.length - 2}.`);
return;
}
const inputFile = args[2];
if (!inputFile.endsWith('.teal.tmpl')) {
console.error('Input file must be a TEAL template program. Expected .teal.tmpl file type.');
process.exitCode = 1;
return;
}
const inputFileContents = fs.readFileSync(inputFile);
const { variables, contract } = generateCompiledContractWithPlaceholders(inputFileContents);
const jsHeader = `// DO NOT EDIT!\n// Generated from ${path.basename(inputFile)}\n`;
const jsContents = jsHeader + prepareFileContents(variables, contract);
const jsPath = path.join(path.dirname(inputFile), path.basename(inputFile) + '.js');
const tokPath = path.join(path.dirname(inputFile), path.basename(inputFile) + '.tok');
fs.writeFileSync(jsPath, jsContents);
fs.writeFileSync(tokPath, contract);
}
try {
main(process.argv);
} catch (err) {
console.error(err);
process.exitCode = 1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment