Skip to content

Commit aceea3a

Browse files
committed
describe sale v4
1 parent 936efa3 commit aceea3a

File tree

7 files changed

+344
-1
lines changed

7 files changed

+344
-1
lines changed

package.json

+6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
"typescript": "^4.7.4"
1616
},
1717
"dependencies": {
18+
"@ton-community/func-js": "^0.7.0",
1819
"@ton.org/func-js": "^0.1.3",
20+
"@ton/core": "^0.59.0",
21+
"@ton/crypto": "^3.3.0",
22+
"@ton/sandbox": "^0.22.0",
23+
"@ton/test-utils": "^0.4.2",
24+
"@ton/ton": "^15.1.0",
1925
"@types/uuid": "^8.3.4",
2026
"bn.js": "^5.2.1",
2127
"chai": "^4.3.6",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import { Address, beginCell, Cell, contractAddress, Dictionary, DictionaryValue, Slice, StateInit } from '@ton/core'
2+
import { NftFixPriceSaleV4R1CodeCell } from './NftFixPriceSaleV4.source'
3+
import { sign, mnemonicNew,mnemonicToPrivateKey } from '@ton/crypto'
4+
import { bufferToInt, toBufferBE } from "../../ton/bigint";
5+
6+
export type JettonPriceType = {
7+
price: bigint,
8+
jettonMaster: Address,
9+
}
10+
11+
export type NftFixPriceSaleV4DR1Data = {
12+
isComplete: boolean
13+
createdAt: number
14+
marketplaceAddress: Address
15+
nftAddress: Address
16+
nftOwnerAddress: Address | null
17+
fullTonPrice: bigint
18+
marketplaceFeeAddress: Address
19+
marketplaceFeePercent: number
20+
royaltyAddress: Address
21+
royaltyPercent: number
22+
soldAtTime: number,
23+
soldQueryId: bigint,
24+
}
25+
26+
export function buildFixPriceV4SaleData(input: Pick<NftFixPriceSaleV4DR1Data, 'nftAddress'|'royaltyAddress'|'royaltyPercent'|'fullTonPrice'|'nftOwnerAddress'>): Omit<NftFixPriceSaleV4DR1Data, 'marketplaceAddress'> {
27+
return {
28+
...input,
29+
isComplete: false,
30+
createdAt: Math.floor(Date.now()/1000),
31+
marketplaceFeeAddress: Address.parse('EQDDuxx7sa3Dt2GE85a0sIHp4GVoa7OKbAanfo3co9H-h06d'),
32+
marketplaceFeePercent: 0.05,
33+
soldAtTime: 0,
34+
soldQueryId: 0n,
35+
}
36+
}
37+
38+
export const OP_FIX_PRICE_V4_DEPLOY_JETTON = 0xfb5dbf47
39+
export const OP_FIX_PRICE_V4_DEPLOY_BLANK = 0x664c0905
40+
export const OP_FIX_PRICE_V4_CHANGE_PRICE = 0xfd135f7b
41+
42+
export const NftFixPriceJettonPriceValue: DictionaryValue<JettonPriceType> = {
43+
serialize(src, builder) {
44+
builder.storeCoins(src.price)
45+
.storeAddress(src.jettonMaster)
46+
},
47+
parse(slice) {
48+
return {
49+
price: slice.loadCoins(),
50+
jettonMaster: slice.loadAddress(),
51+
}
52+
},
53+
}
54+
55+
function assertPercent(x: number) {
56+
if (x < 0) {
57+
throw new Error(`Percent can not be less zero, got ${x}`)
58+
}
59+
if (x > 1) {
60+
throw new Error('Percent should be less than one')
61+
}
62+
const p = Number((x * 100_000).toFixed(0))
63+
if (!Number.isInteger(p) || p < 0) {
64+
throw new Error(`Percent should be integer after multiple 100k ${x} -> ${p}`)
65+
}
66+
return p
67+
}
68+
69+
async function randomKeyPair() {
70+
const mnemonics = await mnemonicNew()
71+
return mnemonicToPrivateKey(mnemonics)
72+
}
73+
74+
export function buildNftFixPriceSaleV4R1Data(cfg: NftFixPriceSaleV4DR1Data & {publicKey: Buffer | null}) {
75+
return beginCell()
76+
.storeBit(cfg.isComplete)
77+
.storeAddress(cfg.marketplaceAddress)
78+
.storeAddress(cfg.nftOwnerAddress)
79+
.storeCoins(cfg.fullTonPrice)
80+
.storeUint(cfg.soldAtTime, 32)
81+
.storeUint(cfg.soldQueryId, 64)
82+
.storeRef(beginCell()
83+
.storeAddress(cfg.marketplaceFeeAddress)
84+
.storeAddress(cfg.royaltyAddress)
85+
.storeUint(assertPercent(cfg.marketplaceFeePercent), 17)
86+
.storeUint(assertPercent(cfg.royaltyPercent), 17)
87+
.storeAddress(cfg.nftAddress)
88+
.storeUint(cfg.createdAt, 32)
89+
.endCell())
90+
.storeDict(undefined) // empty jetton dict
91+
.storeMaybeBuffer(cfg.publicKey, 256 / 8)
92+
.endCell()
93+
}
94+
95+
export function parseNftFixPriceSaleV4R1Data(slice: Slice) {
96+
const isComplete = slice.loadBit()
97+
const marketplaceAddress = slice.loadMaybeAddress()
98+
const nftOwnerAddress = slice.loadMaybeAddress()
99+
const fullTonPrice = slice.loadCoins()
100+
const soldAtTime = slice.loadUint(32)
101+
const soldQueryId = slice.loadUintBig(64)
102+
const staticData = slice.loadRef().beginParse()
103+
const marketplaceFeeAddress = staticData.loadMaybeAddress()
104+
const royaltyAddress = staticData.loadMaybeAddress()
105+
const marketplaceFeePercent = staticData.loadUint(17)
106+
const royaltyPercent = staticData.loadUint(17)
107+
const nftAddress = staticData.loadMaybeAddress()
108+
const createdAt = staticData.loadUint(32)
109+
const jettonPrice = slice.loadDict(Dictionary.Keys.BigUint(256), NftFixPriceJettonPriceValue)
110+
const publicKey = slice.remainingBits > 0 ? slice.loadMaybeUintBig(256) : null
111+
return {
112+
isComplete,
113+
createdAt,
114+
marketplaceAddress,
115+
nftAddress,
116+
nftOwnerAddress,
117+
fullTonPrice,
118+
marketplaceFeeAddress,
119+
marketplaceFeePercent: marketplaceFeePercent / 100_000,
120+
royaltyAddress,
121+
royaltyPercent: royaltyPercent / 100_000,
122+
jettonPrice,
123+
soldAtTime,
124+
soldQueryId,
125+
publicKey: publicKey ? toBufferBE(publicKey, 256 / 8) : null,
126+
}
127+
}
128+
129+
export type NftFixPriceSaleV4DR1DataRelaxed = ReturnType<typeof parseNftFixPriceSaleV4R1Data>
130+
131+
export function isValidFixPriceSaleV4DR1Data(input: NftFixPriceSaleV4DR1DataRelaxed): input is (NftFixPriceSaleV4DR1Data & {
132+
jettonPrice: Dictionary<bigint, JettonPriceType>,
133+
publicKey: Buffer|null
134+
}) {
135+
if (!input.marketplaceAddress) {
136+
throw new Error('marketplaceAddress is null')
137+
}
138+
if (!input.nftAddress) {
139+
throw new Error('nftAddress is null')
140+
}
141+
if (!input.marketplaceFeeAddress) {
142+
throw new Error('marketplaceFeeAddress is null')
143+
}
144+
if (!input.royaltyAddress) {
145+
throw new Error('royaltyAddress is null')
146+
}
147+
148+
if (input.marketplaceAddress.workChain !== 0) {
149+
throw new Error('marketplaceAddress wrong workchain')
150+
}
151+
if (input.nftAddress.workChain !== 0) {
152+
throw new Error('nftAddress wrong workchain')
153+
}
154+
if (input.marketplaceFeeAddress.workChain !== 0) {
155+
throw new Error('marketplaceFeeAddress wrong workchain')
156+
}
157+
if (input.royaltyAddress.workChain !== 0) {
158+
throw new Error('royaltyAddress wrong workchain')
159+
}
160+
if (input.nftOwnerAddress && input.nftOwnerAddress.workChain !== 0) {
161+
throw new Error('nftOwnerAddress wrong workchain')
162+
}
163+
164+
if (input.marketplaceFeePercent < 0 || input.marketplaceFeePercent > 1 || isNaN(input.marketplaceFeePercent)) {
165+
throw new Error('marketplaceFeePercent wrong value')
166+
}
167+
if (input.royaltyPercent < 0 || input.royaltyPercent > 1 || isNaN(input.royaltyPercent)) {
168+
throw new Error('royaltyPercent wrong value')
169+
}
170+
171+
if ((input.marketplaceFeePercent + input.royaltyPercent) > 1) {
172+
throw new Error('fee to big')
173+
}
174+
return true
175+
}
176+
177+
export function nftFixPriceV4CreateDeployMessage(queryId: bigint, marketplaceAddress: Address, jettonPrice: Dictionary<bigint, JettonPriceType>, secretKey: Buffer) {
178+
const signedData = beginCell()
179+
.storeAddress(marketplaceAddress)
180+
.storeDict(jettonPrice)
181+
.endCell()
182+
const signature = sign(signedData.hash(), secretKey)
183+
return beginCell()
184+
.storeUint(OP_FIX_PRICE_V4_DEPLOY_JETTON, 32)
185+
.storeUint(queryId, 64)
186+
.storeBuffer(signature, 512 / 8)
187+
.storeAddress(marketplaceAddress)
188+
.storeDict(jettonPrice)
189+
.endCell()
190+
}
191+
192+
export function nftFixPriceV4ChangePriceMessage(opts: {
193+
queryId: bigint,
194+
newTonPrice: bigint,
195+
newJettonPrice: Dictionary<bigint, JettonPriceType> | null,
196+
}) {
197+
return beginCell()
198+
.storeUint(OP_FIX_PRICE_V4_CHANGE_PRICE, 32)
199+
.storeUint(opts.queryId, 64)
200+
.storeCoins(opts.newTonPrice)
201+
.storeDict(opts.newJettonPrice)
202+
.endCell()
203+
}
204+
205+
export async function buildJettonPriceDict(opts: {
206+
saleAddress: Address,
207+
jettonPrices: { [key: string]: bigint }, // jettonMasterWallet:price
208+
jettonWalletAddressResolver: (jettonMaster: Address, address: Address) => Promise<Address>
209+
}) {
210+
const jettonPricesTags = Object.keys(opts.jettonPrices)
211+
const jettonPriceDict = Dictionary.empty(Dictionary.Keys.BigUint(256), NftFixPriceJettonPriceValue)
212+
for (const jettonMasterStr of jettonPricesTags) {
213+
const jettonMaster = Address.parse(jettonMasterStr)
214+
const jettonWalletAddress = await opts.jettonWalletAddressResolver(jettonMaster, opts.saleAddress)
215+
if (jettonWalletAddress.workChain !== 0) {
216+
throw new Error(`Jetton address has wrong workchain ${jettonWalletAddress.workChain} for jetton ${jettonMasterStr}`)
217+
}
218+
const price = opts.jettonPrices[jettonMasterStr]
219+
if (!price || price <= 0n) {
220+
throw new Error(`Wrong jetton price for jetton ${jettonMasterStr}, ${price}`)
221+
}
222+
jettonPriceDict.set(bufferToInt(jettonWalletAddress.hash), {
223+
jettonMaster,
224+
price,
225+
})
226+
}
227+
return jettonPriceDict
228+
}
229+
230+
export async function buildNftFixPriceSaleV4R1DeployData(opts: {
231+
codeCell?: Cell,
232+
queryId: bigint,
233+
deployerAddress: Address,
234+
marketplaceAddress: Address,
235+
config: Omit<NftFixPriceSaleV4DR1Data, 'marketplaceAddress'>,
236+
jettonPrices: { [key: string]: bigint }, // jettonMasterWallet:price
237+
jettonWalletAddressResolver: (jettonMaster: Address, address: Address) => Promise<Address>
238+
}) {
239+
const keypair = await randomKeyPair()
240+
const dataCell = buildNftFixPriceSaleV4R1Data({
241+
...opts.config,
242+
publicKey: keypair.publicKey,
243+
marketplaceAddress: opts.deployerAddress,
244+
})
245+
const si: StateInit = {
246+
code: opts.codeCell ?? NftFixPriceSaleV4R1CodeCell,
247+
data: dataCell,
248+
}
249+
const saleAddress = contractAddress(0, si)
250+
const jettonPriceDict = await buildJettonPriceDict({
251+
saleAddress,
252+
jettonPrices: opts.jettonPrices,
253+
jettonWalletAddressResolver: opts.jettonWalletAddressResolver,
254+
})
255+
return {
256+
address: saleAddress,
257+
stateInit: si,
258+
message: nftFixPriceV4CreateDeployMessage(opts.queryId, opts.marketplaceAddress, jettonPriceDict, keypair.secretKey),
259+
}
260+
}

packages/ton/bigint.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Convert a BigInt to a big-endian buffer.
3+
* @param num The BigInt to convert.
4+
* @param width The number of bytes that the resulting buffer should be.
5+
* @returns A big-endian buffer representation of num.
6+
*/
7+
export function toBufferBE(num: bigint, width: number): Buffer {
8+
const hex = num.toString(16)
9+
return Buffer.from(hex.padStart(width * 2, '0').slice(0, width * 2), 'hex')
10+
}
11+
12+
export function bufferToInt(b: Buffer): bigint {
13+
return BigInt('0x' + b.toString('hex'))
14+
}

packages/ton/core/func.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import path from 'path'
2+
import fs from 'fs'
3+
import { compileFunc, compilerVersion, ErrorResult } from '@ton-community/func-js'
4+
5+
export class CodeCompileError extends Error {
6+
constructor(message: string, public res: ErrorResult) {
7+
super(message)
8+
}
9+
}
10+
export async function compileFuncCode(funcAbsolutePath:string) {
11+
const cNftCollectionSourceCode = funcAbsolutePath
12+
13+
const cfg = {
14+
targets: [path.basename(cNftCollectionSourceCode)],
15+
sources: (src:string) => {
16+
const file = path.join((path.dirname(cNftCollectionSourceCode)), src)
17+
return fs.readFileSync(path.resolve(file), { encoding: 'utf-8' })
18+
},
19+
}
20+
21+
const ver = await compilerVersion()
22+
const res = await compileFunc(cfg)
23+
24+
if (res.status === 'error') {
25+
throw new CodeCompileError(res.message, res)
26+
} else {
27+
if (res.warnings) {
28+
throw new CodeCompileError(`warnings ${res.warnings}`, {
29+
status: 'error',
30+
message: res.warnings,
31+
snapshot: res.snapshot,
32+
})
33+
}
34+
return {
35+
...res,
36+
compilerVersion: ver.funcVersion,
37+
}
38+
}
39+
}

packages/utils/randomAddressCore.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Address} from "@ton/core";
2+
import { randomBytes } from "crypto";
3+
4+
export function randomAddressCore(prefix?:string) {
5+
if (!prefix) {
6+
return new Address(0, randomBytes(256 / 8))
7+
}
8+
if (prefix.length > 13) {
9+
throw new Error(`Too large prefix: ${prefix}`)
10+
}
11+
const prefixBuffer = Buffer.from(prefix, 'base64')
12+
13+
return new Address(0, Buffer.concat([Buffer.alloc(1).fill(0), prefixBuffer, Buffer.alloc(2).fill(0), randomBytes((256 / 8) - prefixBuffer.length - 3)]))
14+
}

readme.md

+10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ getgems is not responsible for users who use it for the purpose of deception. To
2525

2626
### Actual sale contracts supported by getgems.io
2727

28+
#### [nft-fixprice-sale-v4r1.fc](packages/contracts/sources/nft-fixprice-sale-v4r1.fc)
29+
- code hash base64(a5WmQYucnSNZBF0edVm41UmuDlBvJMqrWPowyPsf64Y=)
30+
- code hash hex(6B95A6418B9C9D2359045D1E7559B8D549AE0E506F24CAAB58FA30C8FB1FEB86)
31+
- code boc NftFixPriceSaleV4R1CodeBoc [NftFixPriceSaleV4.source.ts](packages/contracts/nft-fixprice-sale-v4/NftFixPriceSaleV4.source.ts)
32+
- storage format buildNftFixPriceSaleV4R1Data [NftFixpriceSaleV3.data.ts:74](packages%2Fcontracts%2Fnft-fixprice-sale-v3%2FNftFixpriceSaleV3.data.ts)[NftFixPriceSaleV4.data.ts](packages/contracts/nft-fixprice-sale-v4/NftFixPriceSaleV4.data.ts)
33+
- example testnet https://testnet.tonviewer.com/kQDiSfanFAN3IUZ6wIKy3KHo-inSdWU8oc0BI8TZPYUzNMRJ
34+
- example mainnet https://tonviewer.com/EQD2Kivo-msu8dsQNiMkPHMoEfmNA3oU-ZQ_yWejbN9vXmVu
35+
- contract description (ru): [description-ru.md](packages/contracts/nft-fixprice-sale-v4/description-ru.md)
36+
37+
2838
#### [nft-fixprice-sale-v3r3.fc](packages%2Fcontracts%2Fsources%2Fnft-fixprice-sale-v3r3.fc)
2939
- code hash base64(JCIfpXHlQuBVx3vt/b9SfHr0YM/cfzRMRQeHtM+h600=)
3040
- code hash hex(24221FA571E542E055C77BEDFDBF527C7AF460CFDC7F344C450787B4CFA1EB4D)

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"moduleResolution": "node",
99
"experimentalDecorators": true,
1010
"emitDecoratorMetadata": true,
11-
"sourceMap": false,
11+
"sourceMap": true,
1212
"allowJs": false,
1313
"outDir": "build/packages",
1414
"esModuleInterop": true,

0 commit comments

Comments
 (0)