diff --git a/.luacheckrc b/.luacheckrc index f48231c..e8ea828 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,6 +1,7 @@ allow_defined = true exclude_files = { "dist/", + "src/common/crypto" } globals = { "Handlers", diff --git a/src/common/crypto/cipher/init.lua b/src/common/crypto/cipher/init.lua new file mode 100644 index 0000000..408aa56 --- /dev/null +++ b/src/common/crypto/cipher/init.lua @@ -0,0 +1,8 @@ +local issac = require(".crypto.cipher.issac") + +local cipher = { + _version = "0.0.1", + issac = issac, +} + +return cipher diff --git a/src/common/crypto/cipher/issac.lua b/src/common/crypto/cipher/issac.lua new file mode 100644 index 0000000..4ec39fc --- /dev/null +++ b/src/common/crypto/cipher/issac.lua @@ -0,0 +1,204 @@ +local Hex = require(".crypto.util.hex"); + +-- External Results +local randRsl = {}; +local randCnt = 0; + +-- Internal State +local mm = {}; +local aa,bb,cc = 0,0,0; + +-- Cap to maintain 32 bit maths +local cap = 0x100000000; + +-- CipherMode +local ENCRYPT = 1; +local DECRYPT = 2; + +local function isaac() + cc = ( cc + 1 ) % cap; -- cc just gets incremented once per 256 results + bb = ( bb + cc ) % cap; -- then combined with bb + + for i = 0,255 do + local x = mm[i]; + local y; + local imod = i % 4; + if imod == 0 then aa = aa ~ (aa << 13); + elseif imod == 1 then aa = aa ~ (aa >> 6); + elseif imod == 2 then aa = aa ~ (aa << 2); + elseif imod == 3 then aa = aa ~ (aa >> 16); + end + aa = ( mm[(i+128)%256] + aa ) % cap; + y = ( mm[(x>>2) % 256] + aa + bb ) % cap; + mm[i] = y; + bb = ( mm[(y>>10)%256] + x ) % cap; + randRsl[i] = bb; + end + + randCnt = 0; -- Prepare to use the first set of results. + +end + +local function mix(a) + a[0] = ( a[0] ~ ( a[1] << 11 ) ) % cap; a[3] = ( a[3] + a[0] ) % cap; a[1] = ( a[1] + a[2] ) % cap; + a[1] = ( a[1] ~ ( a[2] >> 2 ) ) % cap; a[4] = ( a[4] + a[1] ) % cap; a[2] = ( a[2] + a[3] ) % cap; + a[2] = ( a[2] ~ ( a[3] << 8 ) ) % cap; a[5] = ( a[5] + a[2] ) % cap; a[3] = ( a[3] + a[4] ) % cap; + a[3] = ( a[3] ~ ( a[4] >> 16 ) ) % cap; a[6] = ( a[6] + a[3] ) % cap; a[4] = ( a[4] + a[5] ) % cap; + a[4] = ( a[4] ~ ( a[5] << 10 ) ) % cap; a[7] = ( a[7] + a[4] ) % cap; a[5] = ( a[5] + a[6] ) % cap; + a[5] = ( a[5] ~ ( a[6] >> 4 ) ) % cap; a[0] = ( a[0] + a[5] ) % cap; a[6] = ( a[6] + a[7] ) % cap; + a[6] = ( a[6] ~ ( a[7] << 8 ) ) % cap; a[1] = ( a[1] + a[6] ) % cap; a[7] = ( a[7] + a[0] ) % cap; + a[7] = ( a[7] ~ ( a[0] >> 9 ) ) % cap; a[2] = ( a[2] + a[7] ) % cap; a[0] = ( a[0] + a[1] ) % cap; +end + +local function randInit(flag) + + -- The golden ratio in 32 bit + -- math.floor((((math.sqrt(5)+1)/2)%1)*2^32) == 2654435769 == 0x9e3779b9 + local a = { [0] = 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, }; + + aa,bb,cc = 0,0,0; + + for i = 1,4 do mix(a) end -- Scramble it. + + for i = 0,255,8 do -- Fill in mm[] with messy stuff. + if flag then -- Use all the information in the seed. + for j = 0,7 do + a[j] = ( a[j] + randRsl[i+j] ) % cap; + end + end + mix(a); + for j = 0,7 do + mm[i+j] = a[j]; + end + end + + if flag then + -- Do a second pass to make all of the seed affect all of mm. + for i = 0,255,8 do + for j = 0,7 do + a[j] = ( a[j] + mm[i+j] ) % cap; + end + mix(a); + for j = 0,7 do + mm[i+j] = a[j]; + end + end + end + + isaac(); -- Fill in the first set of results. + randCnt = 0; -- Prepare to use the first set of results. + +end + +--- Seeds the ISAAC random number generator with the given seed. +--- @param seed string - The seed to use for the random number generator. +--- @param flag? boolean - Whether to use all the information in the seed. Defaults to true. +local function seedIsaac(seed, flag) + local seedLength = #seed; + for i = 0,255 do mm[i] = 0; end + for i = 0,255 do randRsl[i] = seed:byte(i+1,i+1) or 0; end + randInit(flag); +end + +--- Retrieves a random number from the ISAAC random number generator +--- @return number: The random number +local function getRandom() + local result = randRsl[randCnt]; + randCnt = randCnt + 1; + if randCnt > 255 then + isaac(); + randCnt = 0; + end + return result; +end + +--- Get a random 32-bit value within the specified range. +--- @param min? number (optional) - The minimum value of the range. Defaults to 0. +--- @param max? number (optional) - The maximum value of the range. Defaults to 2^31-1. +--- @param seed? string (optional) - The seed to use for the random number generator. +--- @return number: The random 32-bit value within the specified range. +local function random(min, max, seed) + local min = min or 0; + local max = max or 2^31-1; + if seed then + seedIsaac(seed, true); + else + seedIsaac(tostring(math.random(2^31-1)), false); + end + return (getRandom() % (max - min + 1)) + min; +end + + +--- Get a random character in printable ASCII range. +--- @return number: The random character [32, 126]. +local function getRandomChar() + return getRandom() % 95 + 32; +end + +-- Caesar-shift a character places: Generalized Vigenere +local function caesar(m, ch, shift, modulo, start) + local n + local si = 1 + if m == DECRYPT then shift = shift*-1 ; end + n = (ch - start) + shift; + if n < 0 then si,n = -1,n*-1 ; end + n = ( n % modulo ) * si; + if n < 0 then n = n + modulo ; end + return start + n; +end + +--- Encrypts a message using the ISSAC cipher algorithm. +--- @param msg string - The message to be encrypted. +--- @param key string - The key used for encryption. +--- @returns table - A table containing the encrypted message in bytes, string, and hex formats. +local function encrypt(msg, key) + seedIsaac(key, true); + local msgLength = #msg; + local destination = {}; + + for i = 1, msgLength do + destination[i] = string.char(caesar(1, msg:byte(i, i), getRandomChar(), 95, 32)); + end + + local encrypted = destination + + local public = {} + public.asBytes = function() + return encrypted + end + + public.asString = function() + return table.concat(encrypted) + end + + public.asHex = function() + return Hex.stringToHex(table.concat(encrypted)) + end + + return public +end + +--- Decrypts an encrypted message using the ISSAC cipher algorithm. +--- @param encrypted string - The encrypted message to be decrypted. +--- @param key string - The key used for encryption. +--- @returns string - The decrypted message. +local function decrypt(encrypted, key) + seedIsaac(key, true); + local msgLength = #encrypted; + local destination = {}; + + for i = 1, msgLength do + destination[i] = string.char(caesar(2, encrypted:byte(i, i), getRandomChar(), 95, 32)); + end + + return table.concat(destination); +end + +return { + seedIsaac = seedIsaac, + getRandomChar = getRandomChar, + random = random, + getRandom = getRandom, + encrypt = encrypt, + decrypt = decrypt +} \ No newline at end of file diff --git a/src/common/crypto/digest/init.lua b/src/common/crypto/digest/init.lua new file mode 100644 index 0000000..e801076 --- /dev/null +++ b/src/common/crypto/digest/init.lua @@ -0,0 +1,10 @@ +local SHA2_256 = require(".crypto.digest.sha2_256") +local SHA3 = require(".crypto.digest.sha3") + +local digest = { + _version = "0.0.1", + sha2_256 = SHA2_256.sha2_256, + keccak256 = SHA3.keccak256, +} + +return digest diff --git a/src/common/crypto/digest/sha2_256.lua b/src/common/crypto/digest/sha2_256.lua new file mode 100644 index 0000000..a3f11bb --- /dev/null +++ b/src/common/crypto/digest/sha2_256.lua @@ -0,0 +1,383 @@ +local Bit = require(".crypto.util.bit") +local Queue = require(".crypto.util.queue") + +local CONSTANTS = { + 0x428a2f98, + 0x71374491, + 0xb5c0fbcf, + 0xe9b5dba5, + 0x3956c25b, + 0x59f111f1, + 0x923f82a4, + 0xab1c5ed5, + 0xd807aa98, + 0x12835b01, + 0x243185be, + 0x550c7dc3, + 0x72be5d74, + 0x80deb1fe, + 0x9bdc06a7, + 0xc19bf174, + 0xe49b69c1, + 0xefbe4786, + 0x0fc19dc6, + 0x240ca1cc, + 0x2de92c6f, + 0x4a7484aa, + 0x5cb0a9dc, + 0x76f988da, + 0x983e5152, + 0xa831c66d, + 0xb00327c8, + 0xbf597fc7, + 0xc6e00bf3, + 0xd5a79147, + 0x06ca6351, + 0x14292967, + 0x27b70a85, + 0x2e1b2138, + 0x4d2c6dfc, + 0x53380d13, + 0x650a7354, + 0x766a0abb, + 0x81c2c92e, + 0x92722c85, + 0xa2bfe8a1, + 0xa81a664b, + 0xc24b8b70, + 0xc76c51a3, + 0xd192e819, + 0xd6990624, + 0xf40e3585, + 0x106aa070, + 0x19a4c116, + 0x1e376c08, + 0x2748774c, + 0x34b0bcb5, + 0x391c0cb3, + 0x4ed8aa4a, + 0x5b9cca4f, + 0x682e6ff3, + 0x748f82ee, + 0x78a5636f, + 0x84c87814, + 0x8cc70208, + 0x90befffa, + 0xa4506ceb, + 0xbef9a3f7, + 0xc67178f2, +} + +local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + .. "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + +local AND = Bit.band +local OR = Bit.bor +local NOT = Bit.bnot +local XOR = Bit.bxor +local RROT = Bit.rrotate +local LSHIFT = Bit.lshift +local RSHIFT = Bit.rshift + +--SHA2 is big-endian +local bytes2word = function(b0, b1, b2, b3) + local i = b0 + i = LSHIFT(i, 8) + i = OR(i, b1) + i = LSHIFT(i, 8) + i = OR(i, b2) + i = LSHIFT(i, 8) + i = OR(i, b3) + return i +end + +local word2bytes = function(word) + local b0, b1, b2, b3 + b3 = AND(word, 0xFF) + word = RSHIFT(word, 8) + b2 = AND(word, 0xFF) + word = RSHIFT(word, 8) + b1 = AND(word, 0xFF) + word = RSHIFT(word, 8) + b0 = AND(word, 0xFF) + return b0, b1, b2, b3 +end + +local dword2bytes = function(i) + local b4, b5, b6, b7 = word2bytes(i) + local b0, b1, b2, b3 = word2bytes(math.floor(i / 0x100000000)) + return b0, b1, b2, b3, b4, b5, b6, b7 +end + +local SHA2_256 = function() + local queue = Queue() + + local h0 = 0x6a09e667 + local h1 = 0xbb67ae85 + local h2 = 0x3c6ef372 + local h3 = 0xa54ff53a + local h4 = 0x510e527f + local h5 = 0x9b05688c + local h6 = 0x1f83d9ab + local h7 = 0x5be0cd19 + + local public = {} + + local processBlock = function() + local a = h0 + local b = h1 + local c = h2 + local d = h3 + local e = h4 + local f = h5 + local g = h6 + local h = h7 + + local w = {} + + for i = 0, 15 do + w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop()) + end + + for i = 16, 63 do + local s0 = XOR(RROT(w[i - 15], 7), XOR(RROT(w[i - 15], 18), RSHIFT(w[i - 15], 3))) + local s1 = XOR(RROT(w[i - 2], 17), XOR(RROT(w[i - 2], 19), RSHIFT(w[i - 2], 10))) + w[i] = AND(w[i - 16] + s0 + w[i - 7] + s1, 0xFFFFFFFF) + end + + for i = 0, 63 do + local s1 = XOR(RROT(e, 6), XOR(RROT(e, 11), RROT(e, 25))) + local ch = XOR(AND(e, f), AND(NOT(e), g)) + local temp1 = h + s1 + ch + CONSTANTS[i + 1] + w[i] + local s0 = XOR(RROT(a, 2), XOR(RROT(a, 13), RROT(a, 22))) + local maj = XOR(AND(a, b), XOR(AND(a, c), AND(b, c))) + local temp2 = s0 + maj + + h = g + g = f + f = e + e = d + temp1 + d = c + c = b + b = a + a = temp1 + temp2 + end + + h0 = AND(h0 + a, 0xFFFFFFFF) + h1 = AND(h1 + b, 0xFFFFFFFF) + h2 = AND(h2 + c, 0xFFFFFFFF) + h3 = AND(h3 + d, 0xFFFFFFFF) + h4 = AND(h4 + e, 0xFFFFFFFF) + h5 = AND(h5 + f, 0xFFFFFFFF) + h6 = AND(h6 + g, 0xFFFFFFFF) + h7 = AND(h7 + h, 0xFFFFFFFF) + end + + public.init = function() + queue.reset() + + h0 = 0x6a09e667 + h1 = 0xbb67ae85 + h2 = 0x3c6ef372 + h3 = 0xa54ff53a + h4 = 0x510e527f + h5 = 0x9b05688c + h6 = 0x1f83d9ab + h7 = 0x5be0cd19 + + return public + end + + public.update = function(bytes) + for b in bytes do + queue.push(b) + if queue.size() >= 64 then + processBlock() + end + end + + return public + end + + public.finish = function() + local bits = queue.getHead() * 8 + + queue.push(0x80) + while ((queue.size() + 7) % 64) < 63 do + queue.push(0x00) + end + + local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits) + + queue.push(b0) + queue.push(b1) + queue.push(b2) + queue.push(b3) + queue.push(b4) + queue.push(b5) + queue.push(b6) + queue.push(b7) + + while queue.size() > 0 do + processBlock() + end + + return public + end + + public.asBytes = function() + local b0, b1, b2, b3 = word2bytes(h0) + local b4, b5, b6, b7 = word2bytes(h1) + local b8, b9, b10, b11 = word2bytes(h2) + local b12, b13, b14, b15 = word2bytes(h3) + local b16, b17, b18, b19 = word2bytes(h4) + local b20, b21, b22, b23 = word2bytes(h5) + local b24, b25, b26, b27 = word2bytes(h6) + local b28, b29, b30, b31 = word2bytes(h7) + + return { + b0, + b1, + b2, + b3, + b4, + b5, + b6, + b7, + b8, + b9, + b10, + b11, + b12, + b13, + b14, + b15, + b16, + b17, + b18, + b19, + b20, + b21, + b22, + b23, + b24, + b25, + b26, + b27, + b28, + b29, + b30, + b31, + } + end + + public.asHex = function() + local b0, b1, b2, b3 = word2bytes(h0) + local b4, b5, b6, b7 = word2bytes(h1) + local b8, b9, b10, b11 = word2bytes(h2) + local b12, b13, b14, b15 = word2bytes(h3) + local b16, b17, b18, b19 = word2bytes(h4) + local b20, b21, b22, b23 = word2bytes(h5) + local b24, b25, b26, b27 = word2bytes(h6) + local b28, b29, b30, b31 = word2bytes(h7) + + return string.format( + fmt, + b0, + b1, + b2, + b3, + b4, + b5, + b6, + b7, + b8, + b9, + b10, + b11, + b12, + b13, + b14, + b15, + b16, + b17, + b18, + b19, + b20, + b21, + b22, + b23, + b24, + b25, + b26, + b27, + b28, + b29, + b30, + b31 + ) + end + + public.asString = function() + local b0, b1, b2, b3 = word2bytes(h0) + local b4, b5, b6, b7 = word2bytes(h1) + local b8, b9, b10, b11 = word2bytes(h2) + local b12, b13, b14, b15 = word2bytes(h3) + local b16, b17, b18, b19 = word2bytes(h4) + local b20, b21, b22, b23 = word2bytes(h5) + local b24, b25, b26, b27 = word2bytes(h6) + local b28, b29, b30, b31 = word2bytes(h7) + + return string.pack( + string.rep("B", 32), + b0, + b1, + b2, + b3, + b4, + b5, + b6, + b7, + b8, + b9, + b10, + b11, + b12, + b13, + b14, + b15, + b16, + b17, + b18, + b19, + b20, + b21, + b22, + b23, + b24, + b25, + b26, + b27, + b28, + b29, + b30, + b31 + ) + end + + return public +end + +--- @class Stream : table + +--- @param stream (Stream) - A function that returns the next byte of the stream, or nil if the stream has ended. +--- @returns table - A table containing the hash in bytes, string, and hex formats. +local sha2_256 = function(stream) + local result = SHA2_256().update(stream).finish() + return result +end + +return { + sha2_256 = sha2_256, + SHA2_256 = SHA2_256, +} diff --git a/src/common/crypto/digest/sha3.lua b/src/common/crypto/digest/sha3.lua new file mode 100644 index 0000000..bef4656 --- /dev/null +++ b/src/common/crypto/digest/sha3.lua @@ -0,0 +1,235 @@ +local Hex = require(".crypto.util.hex"); + +local ROUNDS = 24 + +local roundConstants = { +0x0000000000000001, +0x0000000000008082, +0x800000000000808A, +0x8000000080008000, +0x000000000000808B, +0x0000000080000001, +0x8000000080008081, +0x8000000000008009, +0x000000000000008A, +0x0000000000000088, +0x0000000080008009, +0x000000008000000A, +0x000000008000808B, +0x800000000000008B, +0x8000000000008089, +0x8000000000008003, +0x8000000000008002, +0x8000000000000080, +0x000000000000800A, +0x800000008000000A, +0x8000000080008081, +0x8000000000008080, +0x0000000080000001, +0x8000000080008008 +} + +local rotationOffsets = { +-- ordered for [x][y] dereferencing, so appear flipped here: +{0, 36, 3, 41, 18}, +{1, 44, 10, 45, 2}, +{62, 6, 43, 15, 61}, +{28, 55, 25, 21, 56}, +{27, 20, 39, 8, 14} +} + + + +-- the full permutation function +local function keccakF(st) + local permuted = st.permuted + local parities = st.parities + for round = 1, ROUNDS do + -- theta() + for x = 1,5 do + parities[x] = 0 + local sx = st[x] + for y = 1,5 do parities[x] = parities[x] ~ sx[y] end + end + -- + -- unroll the following loop + --for x = 1,5 do + -- local p5 = parities[(x)%5 + 1] + -- local flip = parities[(x-2)%5 + 1] ~ ( p5 << 1 | p5 >> 63) + -- for y = 1,5 do st[x][y] = st[x][y] ~ flip end + --end + local p5, flip, s + --x=1 + p5 = parities[2] + flip = parities[5] ~ (p5 << 1 | p5 >> 63) + s = st[1] + for y = 1,5 do s[y] = s[y] ~ flip end + --x=2 + p5 = parities[3] + flip = parities[1] ~ (p5 << 1 | p5 >> 63) + s = st[2] + for y = 1,5 do s[y] = s[y] ~ flip end + --x=3 + p5 = parities[4] + flip = parities[2] ~ (p5 << 1 | p5 >> 63) + s = st[3] + for y = 1,5 do s[y] = s[y] ~ flip end + --x=4 + p5 = parities[5] + flip = parities[3] ~ (p5 << 1 | p5 >> 63) + s = st[4] + for y = 1,5 do s[y] = s[y] ~ flip end + --x=5 + p5 = parities[1] + flip = parities[4] ~ (p5 << 1 | p5 >> 63) + s = st[5] + for y = 1,5 do s[y] = s[y] ~ flip end + + -- rhopi() + for y = 1,5 do + local py = permuted[y] + local r + for x = 1,5 do + s, r = st[x][y], rotationOffsets[x][y] + py[(2*x + 3*y)%5 + 1] = (s << r | s >> (64-r)) + end + end + + -- chi() - unroll the loop + --for x = 1,5 do + -- for y = 1,5 do + -- local combined = (~ permuted[(x)%5 +1][y]) & permuted[(x+1)%5 +1][y] + -- st[x][y] = permuted[x][y] ~ combined + -- end + --end + + local p, p1, p2 + --x=1 + s, p, p1, p2 = st[1], permuted[1], permuted[2], permuted[3] + for y = 1,5 do s[y] = p[y] ~ (~ p1[y]) & p2[y] end + --x=2 + s, p, p1, p2 = st[2], permuted[2], permuted[3], permuted[4] + for y = 1,5 do s[y] = p[y] ~ (~ p1[y]) & p2[y] end + --x=3 + s, p, p1, p2 = st[3], permuted[3], permuted[4], permuted[5] + for y = 1,5 do s[y] = p[y] ~ (~ p1[y]) & p2[y] end + --x=4 + s, p, p1, p2 = st[4], permuted[4], permuted[5], permuted[1] + for y = 1,5 do s[y] = p[y] ~ (~ p1[y]) & p2[y] end + --x=5 + s, p, p1, p2 = st[5], permuted[5], permuted[1], permuted[2] + for y = 1,5 do s[y] = p[y] ~ (~ p1[y]) & p2[y] end + + -- iota() + st[1][1] = st[1][1] ~ roundConstants[round] + end +end + + +local function absorb(st, buffer, algorithm) + + local blockBytes = st.rate / 8 + local blockWords = blockBytes / 8 + + -- append 0x01 byte and pad with zeros to block size (rate/8 bytes) + local totalBytes = #buffer + 1 + -- for keccak (2012 submission), the padding is byte 0x01 followed by zeros + -- for SHA3 (NIST, 2015), the padding is byte 0x06 followed by zeros + + if algorithm == "keccak" then + buffer = buffer .. ( '\x01' .. string.char(0):rep(blockBytes - (totalBytes % blockBytes))) + end + + if algorithm == "sha3" then + buffer = buffer .. ( '\x06' .. string.char(0):rep(blockBytes - (totalBytes % blockBytes))) + end + + totalBytes = #buffer + + --convert data to an array of u64 + local words = {} + for i = 1, totalBytes - (totalBytes % 8), 8 do + words[#words + 1] = string.unpack(' { }); it('should get total supply', async () => { const res = await getTotalSupply(); - assert(res, 'total supply should be equal to 1'); + assert.strictEqual(res, 1, 'total supply should be equal to 1'); + }); + + for (const allowUnsafeAddresses of [false, undefined]) { + it(`should transfer when an invalid address is provided and \`Allow-Unsafe-Addresses\` is ${allowUnsafeAddresses}`, async () => { + const recipient = 'invalid-address'; + const senderBalance = await handle({ + Tags: [{ name: 'Action', value: 'Balance' }], + }); + const senderBalanceData = JSON.parse(senderBalance.Messages[0].Data); + const transferResult = await handle({ + Tags: [ + { name: 'Action', value: 'Transfer' }, + { name: 'Recipient', value: recipient }, + { name: 'Cast', value: true }, + { name: 'Allow-Unsafe-Addresses', value: allowUnsafeAddresses }, + ], + }); + + // assert the error tag + const errorTag = transferResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.ok(errorTag, 'Error tag should be present'); + + const result = await handle( + { + Tags: [{ name: 'Action', value: 'Balances' }], + }, + transferResult.Memory, + ); + const balances = JSON.parse(result.Messages[0].Data); + + assert.equal(balances[recipient], undefined); + assert.equal(balances[STUB_ADDRESS], 1); + }); + } + + /** + * We allow transfers to addresses that may appear invalid if the `Allow-Unsafe-Addresses` tag is true for several reasons: + * 1. To support future address formats and signature schemes that may emerge + * 2. To maintain compatibility with new blockchain networks that could be integrated + * 3. To avoid breaking changes if address validation rules need to be updated + * 4. To give users flexibility in how they structure their addresses + * 5. To reduce protocol-level restrictions that could limit innovation + */ + it('should transfer when an invalid address is provided and `Allow-Unsafe-Addresses` is true', async () => { + const recipient = 'invalid-address'; + const senderBalance = await handle({ + Tags: [{ name: 'Action', value: 'Balance' }], + }); + const senderBalanceData = JSON.parse(senderBalance.Messages[0].Data); + const transferResult = await handle({ + Tags: [ + { name: 'Action', value: 'Transfer' }, + { name: 'Recipient', value: recipient }, + { name: 'Cast', value: true }, + { name: 'Allow-Unsafe-Addresses', value: true }, + ], + }); + + // get balances + const result = await handle( + { + Tags: [{ name: 'Action', value: 'Balances' }], + }, + transferResult.Memory, + ); + const balances = JSON.parse(result.Messages[0].Data); + assert.strictEqual(balances[recipient], 1); + assert.strictEqual(balances[STUB_ADDRESS], undefined); }); }); diff --git a/test/registry.test.mjs b/test/registry.test.mjs index a8e245c..6709b11 100644 --- a/test/registry.test.mjs +++ b/test/registry.test.mjs @@ -84,8 +84,6 @@ describe('Registry Updates', async () => { From: STUB_ADDRESS, }); - console.dir(result, { depth: null }); - const message = result.Messages[1]?.Tags.find( (tag) => tag.name === 'Action' && tag.value === 'Credit-Notice', );