Browse Source

Wrote signing and tests for it

master
Lev 2 years ago
parent
commit
87067210b4
  1. 37
      contracts/imports/dns-utils.fc
  2. 1
      contracts/imports/op-codes.fc
  3. 56
      contracts/main.ts
  4. 41
      contracts/nft-collection.fc
  5. 32
      contracts/nft-item.fc
  6. 2
      test/creation.spec.ts
  7. 56
      test/selling.spec.ts
  8. 71
      test/signing.spec.ts

37
contracts/imports/dns-utils.fc

@ -102,48 +102,49 @@ int check_domain_string(slice domain) {
int len = slice_bits(str); int len = slice_bits(str);
int need_break = 0; int need_break = 0;
builder result = begin_cell(); builder result = begin_cell();
do { int res_len = 0;
while ~ need_break {
need_break = len == 0; need_break = len == 0;
if (~ need_break) { if (~ need_break) {
int char = str~load_uint(8); int char = str~load_uint(8);
int byte = 0; int byte = 0;
if (char >= 65) { if ((char >= 65) & (char <= 90)) {
if (char <= 90) { ;; a-z -> 0-25 ;; Code for A-Z -> 0-25
byte = char - 65; byte = char - 65;
} else { } else {
if (char >= 97) { ;; A-Z -> 26-51 if ((char >= 97) & (char <= 122)) {
if (char <= 122) { ;; Code for a-z -> 26-51
byte = char - 71; byte = char - 71;
} else { } else {
if (char >= 48) { ;; 0-9 -> 52-61 if ((char >= 48) & (char <= 57)) {
if (char <= 57) { ;; Code for 0-9 -> 52-61
byte = char + 4; byte = char + 4;
} else { } else {
if (char == 45) { ;; - -> 62 if (char == 45) {
;; Code for - -> 62
byte = 62; byte = 62;
} else { } else {
if (char == 95) { ;; _ -> 63 if (char == 95) {
;; Code for _ -> 63
byte = 63; byte = 63;
} else { } else {
throw(260); ;; invalid character throw(260); ;; invalid character
} }
} }
} }
} else {
throw(260); ;; invalid character
} }
} }
len -= 8;
if (len > 0) {
result~store_uint(byte, 6);
} else { } else {
throw(260); ;; invalid character ;; Last byte
result~store_uint(byte, 8 - mod(res_len, 8));
} }
res_len += 6;
} }
} else {
throw(260); ;; invalid character
}
result~store_uint(byte, 6);
len -= 8;
} }
} until (need_break);
return result.end_cell().begin_parse(); return result.end_cell().begin_parse();
} }

1
contracts/imports/op-codes.fc

@ -19,6 +19,7 @@ int op::editorship_assigned() asm "0x511a4463 PUSHINT";
int op::new_nft() asm "0x1a039a51 PUSHINT"; int op::new_nft() asm "0x1a039a51 PUSHINT";
int op::instant_buy_new_nft() asm "0x16c7d435 PUSHINT"; int op::instant_buy_new_nft() asm "0x16c7d435 PUSHINT";
int op::init_after_buy() asm "0x437dc408 PUSHINT"; int op::init_after_buy() asm "0x437dc408 PUSHINT";
int op::collect_money() asm "0x2a0c8a20 PUSHINT";
;; DNS ;; DNS
const int op::fill_up = 0x370fec51; const int op::fill_up = 0x370fec51;

56
contracts/main.ts

@ -1,10 +1,11 @@
import BN from "bn.js"; import BN from "bn.js";
import {Cell, beginCell, Address} from "ton"; import {Cell, beginCell, Address, Slice} from "ton";
import {C7Config, SmartContract} from "ton-contract-executor"; import {C7Config, SmartContract} from "ton-contract-executor";
import {encodeOffChainContent, makeSnakeCell} from "./utils"; import {encodeOffChainContent, makeSnakeCell} from "./utils";
import {randomBytes} from "crypto"; import {randomBytes} from "crypto";
import {keyPairFromSeed, KeyPair, sign} from "ton-crypto"; import {keyPairFromSeed, KeyPair, sign, keyPairFromSecretKey} from "ton-crypto";
import {ExpansionPanelActions} from "@material-ui/core"; import {randomAddress} from "../test/helpers";
import {hashCell} from "ton/dist/boc/boc";
// encode contract storage according to save_data() contract method // encode contract storage according to save_data() contract method
@ -37,7 +38,7 @@ export function data(params: { ownerAddress: Address; collectionAddress: Address
} }
export function collectionData(params: { export function collectionData(params: {
code: Cell, ownerAddress: Address, ownerKey: number, code: Cell, ownerAddress: Address, ownerKey: BN,
price_multiplier?: number, price_steepness?: number price_multiplier?: number, price_steepness?: number
}): Cell { }): Cell {
if (params.price_multiplier == undefined) { if (params.price_multiplier == undefined) {
@ -59,8 +60,43 @@ export function auctionWithWinner(winnerAddress: Address) {
return beginCell().storeAddress(winnerAddress).storeCoins(0).storeUint(0, 64) return beginCell().storeAddress(winnerAddress).storeCoins(0).storeUint(0, 64)
} }
export function setContractBalance(contract: SmartContract, balance: number) { export function setContractBalance(contract: SmartContract, balance: number, address?: Address) {
contract.setC7Config({balance: balance}); if (address == undefined) {
address = randomAddress("collection");
}
contract.setC7Config({balance: balance, myself: address});
}
export function asciiEncode(buf: Buffer): string {
let sl = beginCell().storeBuffer(buf).endCell().beginParse();
let result = "";
while (sl.remaining) {
let byte;
if (sl.remaining >= 6) {
byte = sl.readUint(6).toNumber();
} else {
byte = sl.readUint(sl.remaining).toNumber();
}
if (byte < 26) {
result += String.fromCharCode(byte + 65); // 0-25 -> A-Z
} else if (byte < 52) {
result += String.fromCharCode(byte + 97 - 26); // 26-51 -> a-z
} else if (byte < 62) {
result += String.fromCharCode(byte + 48 - 52); // 52-61 -> 0-9
} else if (byte == 62) {
result += '-'; // 62 -> -
} else if (byte == 63) {
result += '_'; // 63 -> _
}
}
return result;
}
export function signBuy(domain: string, collectionAddress: Address, buyerAddress: Address, ownerKey: Buffer): string {
let signData = beginCell().storeAddress(collectionAddress).storeBuffer(Buffer.from(domain))
.storeAddress(buyerAddress).storeUint(0, 2).endCell();
let signature = sign(hashCell(signData), ownerKey);
return asciiEncode(signature);
} }
export function TON(): number { export function TON(): number {
@ -73,11 +109,13 @@ export function transferOwnership(params: { newOwnerAddress: Address }): Cell {
return beginCell().storeUint(0x5fcc3d14, 32).storeUint(0, 64).storeAddress(params.newOwnerAddress).storeAddress(null).storeInt(0, 1).storeCoins(1 * TON()).endCell(); return beginCell().storeUint(0x5fcc3d14, 32).storeUint(0, 64).storeAddress(params.newOwnerAddress).storeAddress(null).storeInt(0, 1).storeCoins(1 * TON()).endCell();
} }
export function createItem(params: { domain: String }): Cell { export function createItem(params: { domain: String, signature?: String }): Cell {
let signature = '000'; if (params.signature == undefined) {
params.signature = "000";
}
return beginCell() return beginCell()
.storeUint(0, 32) .storeUint(0, 32)
.storeRef(makeSnakeCell(Buffer.from(params.domain + ';' + signature))) .storeRef(makeSnakeCell(Buffer.from(params.domain + ';' + params.signature)))
.endCell(); .endCell();
} }

41
contracts/nft-collection.fc

@ -3,8 +3,6 @@
#include "imports/op-codes.fc"; #include "imports/op-codes.fc";
#include "imports/params.fc"; #include "imports/params.fc";
;; -1 if needed, 0 if not
const signature_needed = 0;
;; storage scheme ;; storage scheme
;; cell collection_content ;; cell collection_content
@ -61,6 +59,22 @@ slice calculate_nft_item_address(int wc, cell state_init) {
.begin_parse(); .begin_parse();
} }
() send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline {
var msg = begin_cell()
.store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(op, 32)
.store_uint(query_id, 64);
if (~ builder_null?(payload)) {
msg = msg.store_builder(payload);
}
send_raw_message(msg.end_cell(), send_mode);
}
() deploy_nft_item(int item_index, cell nft_item_code, cell nft_content) impure { () deploy_nft_item(int item_index, cell nft_item_code, cell nft_content) impure {
cell state_init = calculate_nft_item_state_init(item_index, nft_item_code); cell state_init = calculate_nft_item_state_init(item_index, nft_item_code);
slice nft_address = calculate_nft_item_address(workchain(), state_init); slice nft_address = calculate_nft_item_address(workchain(), state_init);
@ -75,7 +89,8 @@ slice calculate_nft_item_address(int wc, cell state_init) {
} }
int verify_signature(slice signature, slice sender_address, slice domain, int owner_key) { int verify_signature(slice signature, slice sender_address, slice domain, int owner_key) {
cell option_data = begin_cell().store_slice(my_address()).store_slice(domain).store_slice(sender_address).end_cell(); cell option_data = begin_cell().store_slice(my_address())
.store_slice(domain).store_slice(sender_address).store_uint(0, 2).end_cell();
return check_signature(slice_hash(option_data.begin_parse()), signature, owner_key); return check_signature(slice_hash(option_data.begin_parse()), signature, owner_key);
} }
@ -105,8 +120,10 @@ int verify_signature(slice signature, slice sender_address, slice domain, int ow
int item_index = slice_hash(domain); int item_index = slice_hash(domain);
slice sender_address = cs~load_msg_addr(); slice sender_address = cs~load_msg_addr();
if (signature_needed) { if (key != 0) {
slice signature = decode_asciicode(signature_encoded); slice signature = decode_asciicode(signature_encoded);
int bbb = signature.preload_uint(8);
;; throw(300 + slice_bits(signature));
throw_unless(205, verify_signature(signature, sender_address, domain, key)); throw_unless(205, verify_signature(signature, sender_address, domain, key));
} }
@ -121,6 +138,13 @@ int verify_signature(slice signature, slice sender_address, slice domain, int ow
if (op == op::fill_up) { ;; just fill-up balance if (op == op::fill_up) { ;; just fill-up balance
return (); return ();
} }
if (op == op::collect_money()) {
slice sender_address = cs~load_msg_addr();
throw_unless(403, equal_slices(sender_address, addr));
send_msg(addr, msg_value, op::collect_money(), 0, null(), 1);
return ();
}
throw(0xffff); throw(0xffff);
} }
@ -146,6 +170,15 @@ int get_price(slice domain) method_id {
return calcprice(domain, pricing); return calcprice(domain, pricing);
} }
int signature_data(slice sender_address, slice domain) method_id {
cell option_data = begin_cell().store_slice(my_address()).store_slice(domain).store_slice(sender_address).end_cell();
return slice_hash(option_data.begin_parse());
}
slice getaddr() method_id {
return my_address();
}
(int, cell) dnsresolve(slice subdomain, int category) method_id { (int, cell) dnsresolve(slice subdomain, int category) method_id {
throw_unless(70, mod(slice_bits(subdomain), 8) == 0); throw_unless(70, mod(slice_bits(subdomain), 8) == 0);

32
contracts/nft-item.fc

@ -119,43 +119,13 @@ int min_tons_for_storage() asm "1000000000 PUSHINT"; ;; 1 TON
int op = in_msg_body.slice_empty?() ? 0 : in_msg_body~load_uint(32); int op = in_msg_body.slice_empty?() ? 0 : in_msg_body~load_uint(32);
if (op == 0) {
if (op == 0) { ;; todo
;; if (auction_complete) {
;; throw_unless(406, equal_slices(sender_address, owner_address)); ;; only owner can fill-up balance, prevent coins lost right after the auction
;; ;; if owner send bid right after auction he can restore it by transfer resonse message
store_data(index, collection_address, owner_address, content, domain, now()); store_data(index, collection_address, owner_address, content, domain, now());
;; } else {
;; throw_unless(407, msg_value >= muldiv(max_bid_amount, 105, 100)); ;; 5% greater then previous bid
;; int amount_to_send = (max_bid_amount > my_balance - min_tons_for_storage()) ? (my_balance - min_tons_for_storage()) : max_bid_amount;
;; if (amount_to_send > 0) {
;; send_msg(max_bid_address, amount_to_send, op::outbid_notification, cur_lt(), null(), 1); ;; pay transfer fees separately
;; }
;; max_bid_amount = msg_value;
;; max_bid_address = sender_address;
;; int delta_time = auction_prolongation - (auction_end_time - now());
;; if (delta_time > 0) {
;; auction_end_time += delta_time;
;; }
;; store_data(index, collection_address, owner_address, content, domain, now());
;; }
return (); return ();
} }
int query_id = in_msg_body~load_uint(64); int query_id = in_msg_body~load_uint(64);
;; if ((auction_complete) & (~ cell_null?(auction))) { ;; take domain after auction ;; todo
;; int balance_without_msg = my_balance - msg_value;
;; int amount_to_send = (max_bid_amount > balance_without_msg - min_tons_for_storage()) ? (balance_without_msg - min_tons_for_storage()) : max_bid_amount;
;; if (amount_to_send > 0) {
;; send_msg(collection_address, amount_to_send, op::fill_up, query_id, null(), 2); ;; ignore errors
;; my_balance -= amount_to_send;
;; }
;; owner_address = max_bid_address;
;; auction = null();
;; store_data(index, collection_address, owner_address, content, domain, last_fill_up_time);
;; }
if (op == op::transfer()) { if (op == op::transfer()) {
throw_unless(401, equal_slices(sender_address, owner_address)); throw_unless(401, equal_slices(sender_address, owner_address));

2
test/creation.spec.ts

@ -36,7 +36,7 @@ describe("Creating items tests", () => {
value: new BN(100 * main.TON()), value: new BN(100 * main.TON()),
}); });
const res = await contract.sendInternalMessage(sendToSelfMessage); const res = await contract.sendInternalMessage(sendToSelfMessage);
console.log(res);
expect(res.type).to.equal("success"); expect(res.type).to.equal("success");
expect(res.exit_code).to.equal(0); expect(res.exit_code).to.equal(0);
}); });

56
test/selling.spec.ts

@ -1,56 +0,0 @@
import chai, { assert, expect } from "chai";
import chaiBN from "chai-bn";
import BN from "bn.js";
chai.use(chaiBN(BN));
import { Builder, Cell, Slice } from "ton";
import { SmartContract } from "ton-contract-executor";
import * as main from "../contracts/main";
import { internalMessage, randomAddress } from "./helpers";
import { hex } from "../build/main.compiled.json";
import { makeSnakeCell } from "../contracts/utils";
import { signVerify } from "ton-crypto";
describe("Selling tests (instant buy)", () => {
let contract: SmartContract;
let debug: boolean = true;
let keyPair = main.genKeyPair();
beforeEach(async () => {
contract = await SmartContract.fromCell(
Cell.fromBoc(hex)[0],
main.data({
ownerAddress: randomAddress("owner"),
code: Cell.fromBoc(hex)[0],
collectionAddress: randomAddress("collection"),
domain: "alice",
publicKey: keyPair.publicKey
}),
{ debug: debug }
);
});
it("Sell", async () => {
main.setContractBalance(contract, 10 * main.TON());
const sellMessage = internalMessage({
from: randomAddress("buyer"),
body: main.instantBuyMessage({
receiverAddress: randomAddress("buyer"),
issuedCollectionAddr: randomAddress("collection"),
price: 10 * main.TON(),
domain: "bob",
privateKey: keyPair.secretKey
}),
value: new BN(10 * main.TON())
});
context("Public key is correct", async () => {
const pubKeyRes = await contract.invokeGetMethod("get_public_key", []);
expect(pubKeyRes.result[0]).to.equal(keyPair.publicKey);
});
const res = await contract.sendInternalMessage(sellMessage);
expect(res.type).to.equal("success");
expect(res.exit_code).to.equal(0);
});
});

71
test/signing.spec.ts

@ -0,0 +1,71 @@
import chai, {assert, expect} from "chai";
import chaiBN from "chai-bn";
import BN from "bn.js";
chai.use(chaiBN(BN));
import {Address, Builder, Cell, contractAddress, Slice} from "ton";
import {runContract, SmartContract} from "ton-contract-executor";
import * as main from "../contracts/main";
import {internalMessage, randomAddress} from "./helpers";
import {hex} from "../build/nft-collection.compiled.json";
import {makeSnakeCell} from "../contracts/utils";
import {keyPairFromSeed, KeyPair, sign, keyPairFromSecretKey} from "ton-crypto";
import {signBuy} from "../contracts/main";
let ownerKeys = keyPairFromSeed(Buffer.from("0000000000000000000000000000000000000000000000000000000000000000", "hex"));
let ownerPubNum = new BN(ownerKeys.publicKey);
let data = main.collectionData({
ownerAddress: randomAddress("owner"),
code: Cell.fromBoc(hex)[0],
ownerKey: ownerPubNum,
});
describe("Creating items tests", () => {
let contract: SmartContract;
let debug: boolean = true;
beforeEach(async () => {
contract = await SmartContract.fromCell(
Cell.fromBoc(hex)[0],
data,
{debug: debug}
);
contract.setC7Config({
myself: randomAddress("collection")
})
});
it("allows to buy an item with a valid signature", async () => {
main.setContractBalance(contract, 10 * main.TON(), randomAddress("collection"));
let ownerAddr = randomAddress("dude");
let signature = signBuy("test", randomAddress("collection"), ownerAddr, ownerKeys.secretKey);
const sendToSelfMessage = internalMessage({
from: ownerAddr,
body: main.createItem({domain: "test", signature: signature}),
value: new BN(100 * main.TON()),
});
const res = await contract.sendInternalMessage(sendToSelfMessage);
// console.log(res);
let fs = require('fs');
fs.writeFile('logs.txt', res.logs, (_: any) => {});
expect(res.type).to.equal("success");
expect(res.exit_code).to.equal(0);
});
it("does not allow to buy an item if the signature is invalid", async () => {
main.setContractBalance(contract, 10 * main.TON(), randomAddress("collection"));
let ownerAddr = randomAddress("dude");
let signature = signBuy("test", randomAddress("collection"), ownerAddr, ownerKeys.secretKey);
const sendToSelfMessage = internalMessage({
from: ownerAddr,
body: main.createItem({domain: "test", signature: signature}),
value: new BN(100 * main.TON()),
});
const res = await contract.sendInternalMessage(sendToSelfMessage);
expect(res.type).to.equal("success");
expect(res.exit_code).to.equal(0);
});
});
Loading…
Cancel
Save