Created
January 19, 2022 18:34
-
-
Save jasonpaulos/38e9ed474e110a2c34b01310dcdbf24a 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 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