@alephium/web3 0.2.0-rc.4 → 0.2.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/contracts/add/add.ral +5 -8
- package/contracts/greeter/greeter.ral +2 -2
- package/contracts/greeter/greeter_interface.ral +1 -0
- package/contracts/greeter_main.ral +0 -2
- package/contracts/main.ral +0 -2
- package/contracts/sub/sub.ral +1 -0
- package/contracts/test/metadata.ral +1 -0
- package/contracts/test/warnings.ral +1 -0
- package/dist/alephium-web3.min.js +1 -1
- package/dist/alephium-web3.min.js.map +1 -1
- package/dist/src/api/api-alephium.d.ts +16 -0
- package/dist/src/api/api-alephium.js +16 -0
- package/dist/src/contract/contract.d.ts +70 -47
- package/dist/src/contract/contract.js +302 -234
- package/gitignore +0 -1
- package/package.json +2 -4
- package/src/api/api-alephium.ts +30 -0
- package/src/contract/contract.ts +385 -350
- package/templates/base/package.json +2 -2
- package/templates/react/package.json +2 -2
- package/test/contract.test.ts +37 -24
- package/test/events.test.ts +14 -9
- package/test/transaction.test.ts +5 -3
package/src/contract/contract.ts
CHANGED
|
@@ -26,165 +26,382 @@ import { node } from '../api'
|
|
|
26
26
|
import { SignDeployContractTxParams, SignExecuteScriptTxParams, SignerWithNodeProvider } from '../signer'
|
|
27
27
|
import * as ralph from './ralph'
|
|
28
28
|
import { bs58, binToHex, contractIdFromAddress, assertType, Eq } from '../utils'
|
|
29
|
+
import { CompileContractResult, CompileScriptResult } from '../api/api-alephium'
|
|
30
|
+
|
|
31
|
+
type FieldsSig = node.FieldsSig
|
|
32
|
+
type EventSig = node.EventSig
|
|
33
|
+
type FunctionSig = node.FunctionSig
|
|
34
|
+
|
|
35
|
+
enum SourceType {
|
|
36
|
+
Contract = 0,
|
|
37
|
+
Script = 1,
|
|
38
|
+
AbstractContract = 2,
|
|
39
|
+
Interface = 3
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type CompilerOptions = {
|
|
43
|
+
errorOnWarnings: boolean
|
|
44
|
+
ignoreUnusedConstantsWarnings: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_COMPILER_OPTIONS: CompilerOptions = {
|
|
48
|
+
errorOnWarnings: true,
|
|
49
|
+
ignoreUnusedConstantsWarnings: true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class TypedMatcher<T extends SourceType> {
|
|
53
|
+
matcher: RegExp
|
|
54
|
+
type: T
|
|
55
|
+
|
|
56
|
+
constructor(pattern: string, type: T) {
|
|
57
|
+
this.matcher = new RegExp(pattern, 'mg')
|
|
58
|
+
this.type = type
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
match(str: string): number {
|
|
62
|
+
const results = str.match(this.matcher)
|
|
63
|
+
return results === null ? 0 : results.length
|
|
64
|
+
}
|
|
65
|
+
}
|
|
29
66
|
|
|
30
67
|
class SourceFile {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
68
|
+
type: SourceType
|
|
69
|
+
contractPath: string
|
|
70
|
+
sourceCode: string
|
|
71
|
+
sourceCodeHash: string
|
|
72
|
+
|
|
73
|
+
getArtifactPath(artifactsRootPath: string): string {
|
|
74
|
+
return artifactsRootPath + this.contractPath.slice(this.contractPath.indexOf('/')) + '.json'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
constructor(type: SourceType, sourceCode: string, contractPath: string) {
|
|
78
|
+
this.type = type
|
|
79
|
+
this.sourceCode = sourceCode
|
|
80
|
+
this.sourceCodeHash = cryptojs.SHA256(sourceCode).toString()
|
|
81
|
+
this.contractPath = contractPath
|
|
46
82
|
}
|
|
47
83
|
}
|
|
48
84
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
85
|
+
class Compiled<T extends Artifact> {
|
|
86
|
+
sourceFile: SourceFile
|
|
87
|
+
artifact: T
|
|
88
|
+
warnings: string[]
|
|
52
89
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
constructor(sourceFile: SourceFile, artifact: T, warnings: string[]) {
|
|
91
|
+
this.sourceFile = sourceFile
|
|
92
|
+
this.artifact = artifact
|
|
93
|
+
this.warnings = warnings
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class ProjectArtifact {
|
|
98
|
+
static readonly artifactFileName = '.project.json'
|
|
99
|
+
|
|
100
|
+
infos: Map<string, { sourceCodeHash: string; warnings: string[] }>
|
|
101
|
+
|
|
102
|
+
constructor(infos: Map<string, { sourceCodeHash: string; warnings: string[] }>) {
|
|
103
|
+
this.infos = infos
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async saveToFile(rootPath: string): Promise<void> {
|
|
107
|
+
const filepath = rootPath + '/' + ProjectArtifact.artifactFileName
|
|
108
|
+
const content = JSON.stringify(Object.fromEntries(this.infos), null, 2)
|
|
109
|
+
return fsPromises.writeFile(filepath, content)
|
|
110
|
+
}
|
|
56
111
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return this._artifactCache.get(codeHash)
|
|
66
|
-
}
|
|
67
|
-
protected static _putArtifactToCache(contract: Contract): void {
|
|
68
|
-
if (!this._artifactCache.has(contract.codeHash)) {
|
|
69
|
-
if (this._artifactCache.size >= this.artifactCacheCapacity) {
|
|
70
|
-
const keyToDelete = this._artifactCache.keys().next().value
|
|
71
|
-
this._artifactCache.delete(keyToDelete)
|
|
112
|
+
sourceHasChanged(files: SourceFile[]): boolean {
|
|
113
|
+
if (files.length !== this.infos.size) {
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
const info = this.infos.get(file.contractPath)
|
|
118
|
+
if (typeof info === 'undefined' || info.sourceCodeHash !== file.sourceCodeHash) {
|
|
119
|
+
return true
|
|
72
120
|
}
|
|
73
|
-
this._artifactCache.set(contract.codeHash, contract)
|
|
74
121
|
}
|
|
122
|
+
return false
|
|
75
123
|
}
|
|
76
124
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
125
|
+
static async from(rootPath: string): Promise<ProjectArtifact | undefined> {
|
|
126
|
+
const filepath = rootPath + '/' + ProjectArtifact.artifactFileName
|
|
127
|
+
if (!fs.existsSync(filepath)) {
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
const content = await fsPromises.readFile(filepath)
|
|
131
|
+
const files = new Map(
|
|
132
|
+
Object.entries<{ sourceCodeHash: string; warnings: string[] }>(JSON.parse(content.toString()))
|
|
133
|
+
)
|
|
134
|
+
return new ProjectArtifact(files)
|
|
80
135
|
}
|
|
136
|
+
}
|
|
81
137
|
|
|
82
|
-
|
|
83
|
-
|
|
138
|
+
export class Project {
|
|
139
|
+
sourceFiles: SourceFile[]
|
|
140
|
+
contracts: Compiled<Contract>[]
|
|
141
|
+
scripts: Compiled<Script>[]
|
|
142
|
+
|
|
143
|
+
readonly contractsRootPath: string
|
|
144
|
+
readonly artifactsRootPath: string
|
|
145
|
+
readonly nodeProvider: NodeProvider
|
|
146
|
+
|
|
147
|
+
static currentProject: Project
|
|
148
|
+
|
|
149
|
+
static readonly abstractContractMatcher = new TypedMatcher<SourceType>(
|
|
150
|
+
'^Abstract Contract [A-Z][a-zA-Z0-9]*',
|
|
151
|
+
SourceType.AbstractContract
|
|
152
|
+
)
|
|
153
|
+
static readonly contractMatcher = new TypedMatcher('^Contract [A-Z][a-zA-Z0-9]*', SourceType.Contract)
|
|
154
|
+
static readonly interfaceMatcher = new TypedMatcher('^Interface [A-Z][a-zA-Z0-9]* \\{', SourceType.Interface)
|
|
155
|
+
static readonly scriptMatcher = new TypedMatcher('^TxScript [A-Z][a-zA-Z0-9]*', SourceType.Script)
|
|
156
|
+
static readonly matchers = [
|
|
157
|
+
Project.abstractContractMatcher,
|
|
158
|
+
Project.contractMatcher,
|
|
159
|
+
Project.interfaceMatcher,
|
|
160
|
+
Project.scriptMatcher
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
private constructor(
|
|
164
|
+
provider: NodeProvider,
|
|
165
|
+
contractsRootPath: string,
|
|
166
|
+
artifactsRootPath: string,
|
|
167
|
+
sourceFiles: SourceFile[],
|
|
168
|
+
contracts: Compiled<Contract>[],
|
|
169
|
+
scripts: Compiled<Script>[]
|
|
170
|
+
) {
|
|
171
|
+
this.nodeProvider = provider
|
|
172
|
+
this.contractsRootPath = contractsRootPath
|
|
173
|
+
this.artifactsRootPath = artifactsRootPath
|
|
174
|
+
this.sourceFiles = sourceFiles
|
|
175
|
+
this.contracts = contracts
|
|
176
|
+
this.scripts = scripts
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private getContractPath(path: string): string {
|
|
180
|
+
return path.startsWith(`./${this.contractsRootPath}`)
|
|
181
|
+
? path.slice(2)
|
|
182
|
+
: path.startsWith(this.contractsRootPath)
|
|
183
|
+
? path
|
|
184
|
+
: this.contractsRootPath + '/' + path
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private static checkCompilerWarnings(warnings: string[], compilerOptions: CompilerOptions): void {
|
|
188
|
+
const remains = compilerOptions.ignoreUnusedConstantsWarnings
|
|
189
|
+
? warnings.filter((s) => !s.includes('unused constants'))
|
|
190
|
+
: warnings
|
|
191
|
+
if (remains.length !== 0) {
|
|
192
|
+
const prefixPerWarning = ' - '
|
|
193
|
+
const warningString = prefixPerWarning + remains.join('\n' + prefixPerWarning)
|
|
194
|
+
const output = 'Compilation warnings:\n' + warningString + '\n'
|
|
195
|
+
if (compilerOptions.errorOnWarnings) {
|
|
196
|
+
throw new Error(output)
|
|
197
|
+
} else {
|
|
198
|
+
console.log(output)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
84
201
|
}
|
|
85
202
|
|
|
86
|
-
static
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
|
|
203
|
+
static contract(path: string, compilerOptions?: Partial<CompilerOptions>): Contract {
|
|
204
|
+
const contractPath = Project.currentProject.getContractPath(path)
|
|
205
|
+
const contract = Project.currentProject.contracts.find((c) => c.sourceFile.contractPath === contractPath)
|
|
206
|
+
if (typeof contract === 'undefined') {
|
|
207
|
+
throw new Error(`Contract ${contractPath} does not exist`)
|
|
91
208
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
209
|
+
Project.checkCompilerWarnings(contract.warnings, { ...DEFAULT_COMPILER_OPTIONS, ...compilerOptions })
|
|
210
|
+
return contract.artifact
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static script(path: string, compilerOptions?: Partial<CompilerOptions>): Script {
|
|
214
|
+
const contractPath = Project.currentProject.getContractPath(path)
|
|
215
|
+
const script = Project.currentProject.scripts.find((c) => c.sourceFile.contractPath === contractPath)
|
|
216
|
+
if (typeof script === 'undefined') {
|
|
217
|
+
throw new Error(`Script ${contractPath} does not exist`)
|
|
218
|
+
}
|
|
219
|
+
Project.checkCompilerWarnings(script.warnings, { ...DEFAULT_COMPILER_OPTIONS, ...compilerOptions })
|
|
220
|
+
return script.artifact
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async saveArtifactsToFile(): Promise<void> {
|
|
224
|
+
const artifactsRootPath = this.artifactsRootPath
|
|
225
|
+
const saveToFile = async function (compiled: Compiled<Artifact>): Promise<void> {
|
|
226
|
+
const artifactPath = compiled.sourceFile.getArtifactPath(artifactsRootPath)
|
|
227
|
+
const folder = artifactPath.slice(0, artifactPath.lastIndexOf('/'))
|
|
228
|
+
if (!fs.existsSync(folder)) {
|
|
229
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
107
230
|
}
|
|
231
|
+
return fsPromises.writeFile(artifactPath, compiled.artifact.toString())
|
|
232
|
+
}
|
|
233
|
+
for (const contract of this.contracts) {
|
|
234
|
+
await saveToFile(contract)
|
|
235
|
+
}
|
|
236
|
+
for (const script of this.scripts) {
|
|
237
|
+
await saveToFile(script)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
contractByCodeHash(codeHash: string): Contract {
|
|
242
|
+
const contract = this.contracts.find((c) => c.artifact.codeHash === codeHash)
|
|
243
|
+
if (typeof contract === 'undefined') {
|
|
244
|
+
throw new Error(`Unknown code with code hash: ${codeHash}`)
|
|
245
|
+
}
|
|
246
|
+
return contract.artifact
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private async saveProjectArtifactToFile(): Promise<void> {
|
|
250
|
+
const files: Map<string, { sourceCodeHash: string; warnings: string[] }> = new Map()
|
|
251
|
+
this.contracts.forEach((c) => {
|
|
252
|
+
files.set(c.sourceFile.contractPath, {
|
|
253
|
+
sourceCodeHash: c.sourceFile.sourceCodeHash,
|
|
254
|
+
warnings: c.warnings
|
|
255
|
+
})
|
|
108
256
|
})
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
contractStr: string,
|
|
115
|
-
importsCache: string[]
|
|
116
|
-
): Promise<string> {
|
|
117
|
-
const localImportsCache: string[] = []
|
|
118
|
-
let result = contractStr.replace(Common.importRegex, (match) => {
|
|
119
|
-
localImportsCache.push(match)
|
|
120
|
-
return ''
|
|
257
|
+
this.scripts.forEach((s) => {
|
|
258
|
+
files.set(s.sourceFile.contractPath, {
|
|
259
|
+
sourceCodeHash: s.sourceFile.sourceCodeHash,
|
|
260
|
+
warnings: s.warnings
|
|
261
|
+
})
|
|
121
262
|
})
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return result
|
|
263
|
+
const compiledSize = this.contracts.length + this.scripts.length
|
|
264
|
+
this.sourceFiles.slice(compiledSize).forEach((c) => {
|
|
265
|
+
files.set(c.contractPath, {
|
|
266
|
+
sourceCodeHash: c.sourceCodeHash,
|
|
267
|
+
warnings: []
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
const projectArtifact = new ProjectArtifact(files)
|
|
271
|
+
await projectArtifact.saveToFile(this.artifactsRootPath)
|
|
134
272
|
}
|
|
135
273
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
274
|
+
private static async compile(
|
|
275
|
+
provider: NodeProvider,
|
|
276
|
+
files: SourceFile[],
|
|
277
|
+
contractsRootPath: string,
|
|
278
|
+
artifactsRootPath: string
|
|
279
|
+
): Promise<Project> {
|
|
280
|
+
const sourceStr = files.map((f) => f.sourceCode).join('\n')
|
|
281
|
+
const result = await provider.contracts.postContractsCompileProject({
|
|
282
|
+
code: sourceStr
|
|
283
|
+
})
|
|
284
|
+
const contracts: Compiled<Contract>[] = []
|
|
285
|
+
const scripts: Compiled<Script>[] = []
|
|
286
|
+
result.contracts.forEach((contractResult, index) => {
|
|
287
|
+
const sourceFile = files[`${index}`]
|
|
288
|
+
const contract = Contract.fromCompileResult(contractResult)
|
|
289
|
+
contracts.push(new Compiled(sourceFile, contract, contractResult.warnings))
|
|
290
|
+
})
|
|
291
|
+
result.scripts.forEach((scriptResult, index) => {
|
|
292
|
+
const sourceFile = files[index + contracts.length]
|
|
293
|
+
const script = Script.fromCompileResult(scriptResult)
|
|
294
|
+
scripts.push(new Compiled(sourceFile, script, scriptResult.warnings))
|
|
295
|
+
})
|
|
296
|
+
const project = new Project(provider, contractsRootPath, artifactsRootPath, files, contracts, scripts)
|
|
297
|
+
await project.saveArtifactsToFile()
|
|
298
|
+
await project.saveProjectArtifactToFile()
|
|
299
|
+
return project
|
|
300
|
+
}
|
|
144
301
|
|
|
145
|
-
|
|
146
|
-
|
|
302
|
+
private static async loadArtifacts(
|
|
303
|
+
provider: NodeProvider,
|
|
304
|
+
files: SourceFile[],
|
|
305
|
+
projectArtifact: ProjectArtifact,
|
|
306
|
+
contractsRootPath: string,
|
|
307
|
+
artifactsRootPath: string
|
|
308
|
+
): Promise<Project> {
|
|
309
|
+
try {
|
|
310
|
+
const contracts: Compiled<Contract>[] = []
|
|
311
|
+
const scripts: Compiled<Script>[] = []
|
|
312
|
+
for (const file of files) {
|
|
313
|
+
const info = projectArtifact.infos.get(file.contractPath)
|
|
314
|
+
if (typeof info === 'undefined') {
|
|
315
|
+
throw Error(`Unable to find project info for ${file.contractPath}, please rebuild the project`)
|
|
316
|
+
}
|
|
317
|
+
const warnings = info.warnings
|
|
318
|
+
const artifactPath = file.getArtifactPath(artifactsRootPath)
|
|
319
|
+
if (file.type === SourceType.Contract) {
|
|
320
|
+
const artifact = await Contract.fromArtifactFile(artifactPath)
|
|
321
|
+
contracts.push(new Compiled(file, artifact, warnings))
|
|
322
|
+
} else if (file.type === SourceType.Script) {
|
|
323
|
+
const artifact = await Script.fromArtifactFile(artifactPath)
|
|
324
|
+
scripts.push(new Compiled(file, artifact, warnings))
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return new Project(provider, contractsRootPath, artifactsRootPath, files, contracts, scripts)
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.log(`Failed to load artifacts, error: ${error}, try to re-compile contracts...`)
|
|
330
|
+
return Project.compile(provider, files, contractsRootPath, artifactsRootPath)
|
|
331
|
+
}
|
|
147
332
|
}
|
|
148
333
|
|
|
149
|
-
static
|
|
150
|
-
|
|
151
|
-
|
|
334
|
+
private static async loadSourceFile(dirPath: string, filename: string): Promise<SourceFile> {
|
|
335
|
+
const contractPath = dirPath + '/' + filename
|
|
336
|
+
if (!filename.endsWith('.ral')) {
|
|
337
|
+
throw new Error(`Invalid filename: ${contractPath}, smart contract file name should end with ".ral"`)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const sourceBuffer = await fsPromises.readFile(contractPath)
|
|
341
|
+
const sourceStr = sourceBuffer.toString()
|
|
342
|
+
const results = this.matchers.map((m) => m.match(sourceStr))
|
|
343
|
+
const matchNumber = results.reduce((a, b) => a + b, 0)
|
|
344
|
+
if (matchNumber === 0) {
|
|
345
|
+
throw new Error(`No contract defined in file: ${contractPath}`)
|
|
346
|
+
}
|
|
347
|
+
if (matchNumber > 1) {
|
|
348
|
+
throw new Error(`Multiple definitions in file: ${contractPath}`)
|
|
152
349
|
}
|
|
350
|
+
const matcherIndex = results.indexOf(1)
|
|
351
|
+
const type = this.matchers[`${matcherIndex}`].type
|
|
352
|
+
return new SourceFile(type, sourceStr, contractPath)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private static async loadSourceFiles(contractsRootPath: string): Promise<SourceFile[]> {
|
|
356
|
+
const loadDir = async function (dirPath: string, results: SourceFile[]): Promise<void> {
|
|
357
|
+
const dirents = await fsPromises.readdir(dirPath, { withFileTypes: true })
|
|
358
|
+
for (const dirent of dirents) {
|
|
359
|
+
if (dirent.isFile()) {
|
|
360
|
+
const file = await Project.loadSourceFile(dirPath, dirent.name)
|
|
361
|
+
results.push(file)
|
|
362
|
+
} else {
|
|
363
|
+
const newPath = dirPath + '/' + dirent.name
|
|
364
|
+
await loadDir(newPath, results)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const sourceFiles: SourceFile[] = []
|
|
369
|
+
await loadDir(contractsRootPath, sourceFiles)
|
|
370
|
+
const contractAndScriptSize = sourceFiles.filter(
|
|
371
|
+
(f) => f.type === SourceType.Contract || f.type === SourceType.Script
|
|
372
|
+
).length
|
|
373
|
+
if (sourceFiles.length === 0 || contractAndScriptSize === 0) {
|
|
374
|
+
throw new Error('Project have no source files')
|
|
375
|
+
}
|
|
376
|
+
return sourceFiles.sort((a, b) => a.type - b.type)
|
|
153
377
|
}
|
|
154
378
|
|
|
155
|
-
|
|
379
|
+
static async build(
|
|
156
380
|
provider: NodeProvider,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
errorOnWarnings: boolean,
|
|
165
|
-
ignoreUnusedConstantsWarnings: boolean
|
|
166
|
-
) => Promise<T>,
|
|
167
|
-
errorOnWarnings: boolean,
|
|
168
|
-
ignoreUnusedConstantsWarnings: boolean
|
|
169
|
-
): Promise<T> {
|
|
170
|
-
Common.checkFileNameExtension(sourceFile.contractPath)
|
|
171
|
-
|
|
172
|
-
const contractStr = await loadContractStr(sourceFile, [])
|
|
173
|
-
const contractHash = cryptojs.SHA256(contractStr).toString()
|
|
174
|
-
const existingContract = this._getArtifactFromCache(contractHash)
|
|
175
|
-
if (typeof existingContract !== 'undefined') {
|
|
176
|
-
return existingContract as unknown as T
|
|
381
|
+
contractsRootPath = 'contracts',
|
|
382
|
+
artifactsRootPath = 'artifacts'
|
|
383
|
+
): Promise<void> {
|
|
384
|
+
const sourceFiles = await Project.loadSourceFiles(contractsRootPath)
|
|
385
|
+
const projectArtifact = await ProjectArtifact.from(artifactsRootPath)
|
|
386
|
+
if (typeof projectArtifact === 'undefined' || projectArtifact.sourceHasChanged(sourceFiles)) {
|
|
387
|
+
Project.currentProject = await Project.compile(provider, sourceFiles, contractsRootPath, artifactsRootPath)
|
|
177
388
|
} else {
|
|
178
|
-
|
|
389
|
+
Project.currentProject = await Project.loadArtifacts(
|
|
390
|
+
provider,
|
|
391
|
+
sourceFiles,
|
|
392
|
+
projectArtifact,
|
|
393
|
+
contractsRootPath,
|
|
394
|
+
artifactsRootPath
|
|
395
|
+
)
|
|
179
396
|
}
|
|
180
397
|
}
|
|
398
|
+
}
|
|
181
399
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return fsPromises.writeFile(sourceFile.artifactPath, this.toString())
|
|
400
|
+
export abstract class Artifact {
|
|
401
|
+
readonly functions: FunctionSig[]
|
|
402
|
+
|
|
403
|
+
constructor(functions: FunctionSig[]) {
|
|
404
|
+
this.functions = functions
|
|
188
405
|
}
|
|
189
406
|
|
|
190
407
|
abstract buildByteCodeToDeploy(initialFields?: Fields): string
|
|
@@ -200,123 +417,31 @@ export abstract class Common {
|
|
|
200
417
|
usingAssetsInContractFunctions(): string[] {
|
|
201
418
|
return this.functions.filter((func) => func.useAssetsInContract).map((func) => func.name)
|
|
202
419
|
}
|
|
203
|
-
|
|
204
|
-
protected static checkCompilerWarnings(
|
|
205
|
-
compiled: { warnings: string[] },
|
|
206
|
-
errorOnWarnings: boolean,
|
|
207
|
-
ignoreUnusedConstantsWarnings: boolean
|
|
208
|
-
): void {
|
|
209
|
-
const warnings = ignoreUnusedConstantsWarnings
|
|
210
|
-
? compiled.warnings.filter((s) => !s.includes('unused constants'))
|
|
211
|
-
: compiled.warnings
|
|
212
|
-
if (warnings.length !== 0) {
|
|
213
|
-
const prefixPerWarning = ' - '
|
|
214
|
-
const warningString = prefixPerWarning + warnings.join('\n' + prefixPerWarning)
|
|
215
|
-
const output = 'Compilation warnings:\n' + warningString + '\n'
|
|
216
|
-
if (errorOnWarnings) {
|
|
217
|
-
throw new Error(output)
|
|
218
|
-
} else {
|
|
219
|
-
console.log(output)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
420
|
}
|
|
224
421
|
|
|
225
|
-
export class Contract extends
|
|
422
|
+
export class Contract extends Artifact {
|
|
226
423
|
readonly bytecode: string
|
|
227
424
|
readonly codeHash: string
|
|
228
425
|
readonly fieldsSig: FieldsSig
|
|
229
426
|
readonly eventsSig: EventSig[]
|
|
230
427
|
|
|
231
428
|
constructor(
|
|
232
|
-
sourceCodeSha256: string,
|
|
233
429
|
bytecode: string,
|
|
234
430
|
codeHash: string,
|
|
235
431
|
fieldsSig: FieldsSig,
|
|
236
432
|
eventsSig: EventSig[],
|
|
237
433
|
functions: FunctionSig[]
|
|
238
434
|
) {
|
|
239
|
-
super(
|
|
435
|
+
super(functions)
|
|
240
436
|
this.bytecode = bytecode
|
|
241
437
|
this.codeHash = codeHash
|
|
242
438
|
this.fieldsSig = fieldsSig
|
|
243
439
|
this.eventsSig = eventsSig
|
|
244
440
|
}
|
|
245
441
|
|
|
246
|
-
static checkCodeType(fileName: string, contractStr: string): void {
|
|
247
|
-
const interfaceMatches = contractStr.match(Contract.interfaceRegex)
|
|
248
|
-
const contractMatches = contractStr.match(Contract.contractRegex)
|
|
249
|
-
if (interfaceMatches === null && contractMatches === null) {
|
|
250
|
-
throw new Error(`No contract found in: ${fileName}`)
|
|
251
|
-
}
|
|
252
|
-
if (interfaceMatches && contractMatches) {
|
|
253
|
-
throw new Error(`Multiple contracts and interfaces in: ${fileName}`)
|
|
254
|
-
}
|
|
255
|
-
if (interfaceMatches === null) {
|
|
256
|
-
if (contractMatches !== null && contractMatches.length > 1) {
|
|
257
|
-
throw new Error(`Multiple contracts in: ${fileName}`)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
if (contractMatches === null) {
|
|
261
|
-
if (interfaceMatches !== null && interfaceMatches.length > 1) {
|
|
262
|
-
throw new Error(`Multiple interfaces in: ${fileName}`)
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
private static async loadContractStr(sourceFile: SourceFile): Promise<string> {
|
|
268
|
-
return Common._loadContractStr(sourceFile, [], (code) => Contract.checkCodeType(sourceFile.contractPath, code))
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
static async fromSource(
|
|
272
|
-
provider: NodeProvider,
|
|
273
|
-
path: string,
|
|
274
|
-
errorOnWarnings = true,
|
|
275
|
-
ignoreUnusedConstantsWarnings = true
|
|
276
|
-
): Promise<Contract> {
|
|
277
|
-
if (!fs.existsSync(Common._artifactsFolder())) {
|
|
278
|
-
fs.mkdirSync(Common._artifactsFolder(), { recursive: true })
|
|
279
|
-
}
|
|
280
|
-
const sourceFile = this.getSourceFile(path, [])
|
|
281
|
-
const contract = await Common._from(
|
|
282
|
-
provider,
|
|
283
|
-
sourceFile,
|
|
284
|
-
Contract.loadContractStr,
|
|
285
|
-
Contract.compile,
|
|
286
|
-
errorOnWarnings,
|
|
287
|
-
ignoreUnusedConstantsWarnings
|
|
288
|
-
)
|
|
289
|
-
this._putArtifactToCache(contract)
|
|
290
|
-
return contract
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private static async compile(
|
|
294
|
-
provider: NodeProvider,
|
|
295
|
-
sourceFile: SourceFile,
|
|
296
|
-
contractStr: string,
|
|
297
|
-
contractHash: string,
|
|
298
|
-
errorOnWarnings: boolean,
|
|
299
|
-
ignoreUnusedConstantsWarnings: boolean
|
|
300
|
-
): Promise<Contract> {
|
|
301
|
-
const compiled = await provider.contracts.postContractsCompileContract({ code: contractStr })
|
|
302
|
-
Common.checkCompilerWarnings(compiled, errorOnWarnings, ignoreUnusedConstantsWarnings)
|
|
303
|
-
|
|
304
|
-
const artifact = new Contract(
|
|
305
|
-
contractHash,
|
|
306
|
-
compiled.bytecode,
|
|
307
|
-
compiled.codeHash,
|
|
308
|
-
compiled.fields,
|
|
309
|
-
compiled.events,
|
|
310
|
-
compiled.functions
|
|
311
|
-
)
|
|
312
|
-
await artifact._saveToFile(sourceFile)
|
|
313
|
-
return artifact
|
|
314
|
-
}
|
|
315
|
-
|
|
316
442
|
// TODO: safely parse json
|
|
317
443
|
static fromJson(artifact: any): Contract {
|
|
318
444
|
if (
|
|
319
|
-
artifact.sourceCodeSha256 == null ||
|
|
320
445
|
artifact.bytecode == null ||
|
|
321
446
|
artifact.codeHash == null ||
|
|
322
447
|
artifact.fieldsSig == null ||
|
|
@@ -326,44 +451,42 @@ export class Contract extends Common {
|
|
|
326
451
|
throw Error('The artifact JSON for contract is incomplete')
|
|
327
452
|
}
|
|
328
453
|
const contract = new Contract(
|
|
329
|
-
artifact.sourceCodeSha256,
|
|
330
454
|
artifact.bytecode,
|
|
331
455
|
artifact.codeHash,
|
|
332
456
|
artifact.fieldsSig,
|
|
333
457
|
artifact.eventsSig,
|
|
334
458
|
artifact.functions
|
|
335
459
|
)
|
|
336
|
-
this._putArtifactToCache(contract)
|
|
337
460
|
return contract
|
|
338
461
|
}
|
|
339
462
|
|
|
463
|
+
static fromCompileResult(result: CompileContractResult): Contract {
|
|
464
|
+
return new Contract(result.bytecode, result.codeHash, result.fields, result.events, result.functions)
|
|
465
|
+
}
|
|
466
|
+
|
|
340
467
|
// support both 'code.ral' and 'code.ral.json'
|
|
341
468
|
static async fromArtifactFile(path: string): Promise<Contract> {
|
|
342
|
-
const
|
|
343
|
-
const artifactPath = sourceFile.artifactPath
|
|
344
|
-
const content = await fsPromises.readFile(artifactPath)
|
|
469
|
+
const content = await fsPromises.readFile(path)
|
|
345
470
|
const artifact = JSON.parse(content.toString())
|
|
346
471
|
return Contract.fromJson(artifact)
|
|
347
472
|
}
|
|
348
473
|
|
|
349
|
-
async fetchState(
|
|
350
|
-
const state = await
|
|
474
|
+
async fetchState(address: string, group: number): Promise<ContractState> {
|
|
475
|
+
const state = await Project.currentProject.nodeProvider.contracts.getContractsAddressState(address, {
|
|
476
|
+
group: group
|
|
477
|
+
})
|
|
351
478
|
return this.fromApiContractState(state)
|
|
352
479
|
}
|
|
353
480
|
|
|
354
481
|
override toString(): string {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
},
|
|
364
|
-
null,
|
|
365
|
-
2
|
|
366
|
-
)
|
|
482
|
+
const object = {
|
|
483
|
+
bytecode: this.bytecode,
|
|
484
|
+
codeHash: this.codeHash,
|
|
485
|
+
fieldsSig: this.fieldsSig,
|
|
486
|
+
eventsSig: this.eventsSig,
|
|
487
|
+
functions: this.functions
|
|
488
|
+
}
|
|
489
|
+
return JSON.stringify(object, null, 2)
|
|
367
490
|
}
|
|
368
491
|
|
|
369
492
|
toState(fields: Fields, asset: Asset, address?: string): ContractState {
|
|
@@ -386,14 +509,13 @@ export class Contract extends Common {
|
|
|
386
509
|
}
|
|
387
510
|
|
|
388
511
|
private async _test(
|
|
389
|
-
provider: NodeProvider,
|
|
390
512
|
funcName: string,
|
|
391
513
|
params: TestContractParams,
|
|
392
514
|
expectPublic: boolean,
|
|
393
515
|
accessType: string
|
|
394
516
|
): Promise<TestContractResult> {
|
|
395
517
|
const apiParams: node.TestContract = this.toTestContract(funcName, params)
|
|
396
|
-
const apiResult = await
|
|
518
|
+
const apiResult = await Project.currentProject.nodeProvider.contracts.postContractsTestContract(apiParams)
|
|
397
519
|
|
|
398
520
|
const methodIndex =
|
|
399
521
|
typeof params.testMethodIndex !== 'undefined' ? params.testMethodIndex : this.getMethodIndex(funcName)
|
|
@@ -406,20 +528,12 @@ export class Contract extends Common {
|
|
|
406
528
|
}
|
|
407
529
|
}
|
|
408
530
|
|
|
409
|
-
async testPublicMethod(
|
|
410
|
-
|
|
411
|
-
funcName: string,
|
|
412
|
-
params: TestContractParams
|
|
413
|
-
): Promise<TestContractResult> {
|
|
414
|
-
return this._test(provider, funcName, params, true, 'public')
|
|
531
|
+
async testPublicMethod(funcName: string, params: TestContractParams): Promise<TestContractResult> {
|
|
532
|
+
return this._test(funcName, params, true, 'public')
|
|
415
533
|
}
|
|
416
534
|
|
|
417
|
-
async testPrivateMethod(
|
|
418
|
-
|
|
419
|
-
funcName: string,
|
|
420
|
-
params: TestContractParams
|
|
421
|
-
): Promise<TestContractResult> {
|
|
422
|
-
return this._test(provider, funcName, params, false, 'private')
|
|
535
|
+
async testPrivateMethod(funcName: string, params: TestContractParams): Promise<TestContractResult> {
|
|
536
|
+
return this._test(funcName, params, false, 'private')
|
|
423
537
|
}
|
|
424
538
|
|
|
425
539
|
toApiFields(fields?: Fields): node.Val[] {
|
|
@@ -465,33 +579,8 @@ export class Contract extends Common {
|
|
|
465
579
|
}
|
|
466
580
|
}
|
|
467
581
|
|
|
468
|
-
static async fromCodeHash(codeHash: string): Promise<Contract> {
|
|
469
|
-
const cached = this._getArtifactFromCache(codeHash)
|
|
470
|
-
if (typeof cached !== 'undefined') {
|
|
471
|
-
return cached as Contract
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const files = await fsPromises.readdir(Common._artifactsFolder())
|
|
475
|
-
for (const file of files) {
|
|
476
|
-
if (file.endsWith('.ral.json')) {
|
|
477
|
-
try {
|
|
478
|
-
const contract = await Contract.fromArtifactFile(file)
|
|
479
|
-
if (contract.codeHash === codeHash) {
|
|
480
|
-
return contract as Contract
|
|
481
|
-
}
|
|
482
|
-
} catch (_) {}
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
throw new Error(`Unknown code with code hash: ${codeHash}`)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
static async getFieldsSig(state: node.ContractState): Promise<FieldsSig> {
|
|
490
|
-
return Contract.fromCodeHash(state.codeHash).then((contract) => contract.fieldsSig)
|
|
491
|
-
}
|
|
492
|
-
|
|
493
582
|
async fromApiContractState(state: node.ContractState): Promise<ContractState> {
|
|
494
|
-
const contract =
|
|
583
|
+
const contract = Project.currentProject.contractByCodeHash(state.codeHash)
|
|
495
584
|
return {
|
|
496
585
|
address: state.address,
|
|
497
586
|
contractId: binToHex(contractIdFromAddress(state.address)),
|
|
@@ -499,7 +588,7 @@ export class Contract extends Common {
|
|
|
499
588
|
initialStateHash: state.initialStateHash,
|
|
500
589
|
codeHash: state.codeHash,
|
|
501
590
|
fields: fromApiFields(state.fields, contract.fieldsSig),
|
|
502
|
-
fieldsSig:
|
|
591
|
+
fieldsSig: contract.fieldsSig,
|
|
503
592
|
asset: fromApiAsset(state.asset)
|
|
504
593
|
}
|
|
505
594
|
}
|
|
@@ -527,7 +616,7 @@ export class Contract extends Common {
|
|
|
527
616
|
} else if (event.eventIndex == -2) {
|
|
528
617
|
eventSig = this.ContractDestroyedEvent
|
|
529
618
|
} else {
|
|
530
|
-
const contract =
|
|
619
|
+
const contract = Project.currentProject.contractByCodeHash(codeHash!)
|
|
531
620
|
eventSig = contract.eventsSig[event.eventIndex]
|
|
532
621
|
}
|
|
533
622
|
|
|
@@ -595,95 +684,41 @@ export class Contract extends Common {
|
|
|
595
684
|
}
|
|
596
685
|
}
|
|
597
686
|
|
|
598
|
-
export class Script extends
|
|
687
|
+
export class Script extends Artifact {
|
|
599
688
|
readonly bytecodeTemplate: string
|
|
600
689
|
readonly fieldsSig: FieldsSig
|
|
601
690
|
|
|
602
|
-
constructor(
|
|
603
|
-
super(
|
|
691
|
+
constructor(bytecodeTemplate: string, fieldsSig: FieldsSig, functions: FunctionSig[]) {
|
|
692
|
+
super(functions)
|
|
604
693
|
this.bytecodeTemplate = bytecodeTemplate
|
|
605
694
|
this.fieldsSig = fieldsSig
|
|
606
695
|
}
|
|
607
696
|
|
|
608
|
-
static
|
|
609
|
-
|
|
610
|
-
if (scriptMatches === null) {
|
|
611
|
-
throw new Error(`No script found in: ${fileName}`)
|
|
612
|
-
} else if (scriptMatches.length > 1) {
|
|
613
|
-
throw new Error(`Multiple scripts in: ${fileName}`)
|
|
614
|
-
} else {
|
|
615
|
-
return
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
private static async loadContractStr(sourceFile: SourceFile): Promise<string> {
|
|
620
|
-
return Common._loadContractStr(sourceFile, [], (code) => Script.checkCodeType(sourceFile.contractPath, code))
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
static async fromSource(
|
|
624
|
-
provider: NodeProvider,
|
|
625
|
-
path: string,
|
|
626
|
-
errorOnWarnings = true,
|
|
627
|
-
ignoreUnusedConstantsWarnings = true
|
|
628
|
-
): Promise<Script> {
|
|
629
|
-
const sourceFile = this.getSourceFile(path, [])
|
|
630
|
-
return Common._from(
|
|
631
|
-
provider,
|
|
632
|
-
sourceFile,
|
|
633
|
-
(sourceFile) => Script.loadContractStr(sourceFile),
|
|
634
|
-
Script.compile,
|
|
635
|
-
errorOnWarnings,
|
|
636
|
-
ignoreUnusedConstantsWarnings
|
|
637
|
-
)
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
private static async compile(
|
|
641
|
-
provider: NodeProvider,
|
|
642
|
-
sourceFile: SourceFile,
|
|
643
|
-
scriptStr: string,
|
|
644
|
-
contractHash: string,
|
|
645
|
-
errorOnWarnings = true,
|
|
646
|
-
ignoreUnusedConstantsWarnings = true
|
|
647
|
-
): Promise<Script> {
|
|
648
|
-
const compiled = await provider.contracts.postContractsCompileScript({ code: scriptStr })
|
|
649
|
-
Common.checkCompilerWarnings(compiled, errorOnWarnings, ignoreUnusedConstantsWarnings)
|
|
650
|
-
const artifact = new Script(contractHash, compiled.bytecodeTemplate, compiled.fields, compiled.functions)
|
|
651
|
-
await artifact._saveToFile(sourceFile)
|
|
652
|
-
return artifact
|
|
697
|
+
static fromCompileResult(result: CompileScriptResult): Script {
|
|
698
|
+
return new Script(result.bytecodeTemplate, result.fields, result.functions)
|
|
653
699
|
}
|
|
654
700
|
|
|
655
701
|
// TODO: safely parse json
|
|
656
702
|
static fromJson(artifact: any): Script {
|
|
657
|
-
if (
|
|
658
|
-
artifact.sourceCodeSha256 == null ||
|
|
659
|
-
artifact.bytecodeTemplate == null ||
|
|
660
|
-
artifact.fieldsSig == null ||
|
|
661
|
-
artifact.functions == null
|
|
662
|
-
) {
|
|
703
|
+
if (artifact.bytecodeTemplate == null || artifact.fieldsSig == null || artifact.functions == null) {
|
|
663
704
|
throw Error('The artifact JSON for script is incomplete')
|
|
664
705
|
}
|
|
665
|
-
return new Script(artifact.
|
|
706
|
+
return new Script(artifact.bytecodeTemplate, artifact.fieldsSig, artifact.functions)
|
|
666
707
|
}
|
|
667
708
|
|
|
668
709
|
static async fromArtifactFile(path: string): Promise<Script> {
|
|
669
|
-
const
|
|
670
|
-
const artifactPath = sourceFile.artifactPath
|
|
671
|
-
const content = await fsPromises.readFile(artifactPath)
|
|
710
|
+
const content = await fsPromises.readFile(path)
|
|
672
711
|
const artifact = JSON.parse(content.toString())
|
|
673
712
|
return this.fromJson(artifact)
|
|
674
713
|
}
|
|
675
714
|
|
|
676
715
|
override toString(): string {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
},
|
|
684
|
-
null,
|
|
685
|
-
2
|
|
686
|
-
)
|
|
716
|
+
const object = {
|
|
717
|
+
bytecodeTemplate: this.bytecodeTemplate,
|
|
718
|
+
fieldsSig: this.fieldsSig,
|
|
719
|
+
functions: this.functions
|
|
720
|
+
}
|
|
721
|
+
return JSON.stringify(object, null, 2)
|
|
687
722
|
}
|
|
688
723
|
|
|
689
724
|
async paramsForDeployment(params: BuildExecuteScriptTx): Promise<SignExecuteScriptTxParams> {
|