@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.
@@ -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
- readonly dirs: string[]
32
- readonly dirPath: string
33
- readonly contractPath: string
34
- readonly artifactPath: string
35
-
36
- constructor(dirs: string[], fileName: string) {
37
- this.dirs = dirs
38
- this.dirPath = dirs.length === 0 ? '' : dirs.join('/') + '/'
39
- if (fileName.endsWith('.json')) {
40
- this.contractPath = './contracts/' + this.dirPath + fileName.slice(0, -5)
41
- this.artifactPath = './artifacts/' + this.dirPath + fileName
42
- } else {
43
- this.contractPath = './contracts/' + this.dirPath + fileName
44
- this.artifactPath = './artifacts/' + this.dirPath + fileName + '.json'
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
- type FieldsSig = node.FieldsSig
50
- type EventSig = node.EventSig
51
- type FunctionSig = node.FunctionSig
85
+ class Compiled<T extends Artifact> {
86
+ sourceFile: SourceFile
87
+ artifact: T
88
+ warnings: string[]
52
89
 
53
- export abstract class Common {
54
- readonly sourceCodeSha256: string
55
- readonly functions: FunctionSig[]
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
- static readonly importRegex = new RegExp('^import "([^"/]+/(([^"]+)/)?)?[a-z][a-z_0-9]*.ral"', 'mg')
58
- static readonly contractRegex = new RegExp('^(Abstract[ ]+)?Contract [A-Z][a-zA-Z0-9]*', 'mg')
59
- static readonly interfaceRegex = new RegExp('^Interface [A-Z][a-zA-Z0-9]* \\{', 'mg')
60
- static readonly scriptRegex = new RegExp('^TxScript [A-Z][a-zA-Z0-9]*', 'mg')
61
-
62
- private static _artifactCache: Map<string, Contract | Script> = new Map<string, Contract | Script>()
63
- static artifactCacheCapacity = 20
64
- protected static _getArtifactFromCache(codeHash: string): Contract | Script | undefined {
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
- constructor(sourceCodeSha256: string, functions: FunctionSig[]) {
78
- this.sourceCodeSha256 = sourceCodeSha256
79
- this.functions = functions
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
- protected static _artifactsFolder(): string {
83
- return './artifacts/'
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 getSourceFile(path: string, _dirs: string[]): SourceFile {
87
- const parts = path.split('/')
88
- const dirs = Array.from(_dirs)
89
- if (parts.length === 1) {
90
- return new SourceFile(dirs, path)
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
- parts.slice(0, parts.length - 1).forEach((part) => {
93
- switch (part) {
94
- case '.': {
95
- break
96
- }
97
- case '..': {
98
- if (dirs.length === 0) {
99
- throw new Error('Invalid file path: ' + path)
100
- }
101
- dirs.pop()
102
- break
103
- }
104
- default: {
105
- dirs.push(part)
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
- return new SourceFile(dirs, parts[parts.length - 1])
110
- }
111
-
112
- protected static async _handleImports(
113
- pathes: string[],
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
- for (const myImport of localImportsCache) {
123
- const relativePath = myImport.slice(8, -1)
124
- const importSourceFile = this.getSourceFile(relativePath, pathes)
125
- if (!importsCache.includes(importSourceFile.contractPath)) {
126
- importsCache.push(importSourceFile.contractPath)
127
- const importContractStr = await Common._loadContractStr(importSourceFile, importsCache, (code) =>
128
- Contract.checkCodeType(importSourceFile.contractPath, code)
129
- )
130
- result = result.concat('\n', importContractStr)
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
- protected static async _loadContractStr(
137
- sourceFile: SourceFile,
138
- importsCache: string[],
139
- validate: (code: string) => void
140
- ): Promise<string> {
141
- const contractPath = sourceFile.contractPath
142
- const contractBuffer = await fsPromises.readFile(contractPath)
143
- const contractStr = contractBuffer.toString()
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
- validate(contractStr)
146
- return Common._handleImports(sourceFile.dirs, contractStr, importsCache)
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 checkFileNameExtension(fileName: string): void {
150
- if (!fileName.endsWith('.ral')) {
151
- throw new Error('Smart contract file name should end with ".ral"')
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
- protected static async _from<T extends { sourceCodeSha256: string }>(
379
+ static async build(
156
380
  provider: NodeProvider,
157
- sourceFile: SourceFile,
158
- loadContractStr: (sourceFile: SourceFile, importsCache: string[]) => Promise<string>,
159
- compile: (
160
- provider: NodeProvider,
161
- sourceFile: SourceFile,
162
- contractStr: string,
163
- contractHash: string,
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
- return compile(provider, sourceFile, contractStr, contractHash, errorOnWarnings, ignoreUnusedConstantsWarnings)
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
- protected _saveToFile(sourceFile: SourceFile): Promise<void> {
183
- const folder = Common._artifactsFolder() + sourceFile.dirPath
184
- if (!fs.existsSync(folder)) {
185
- fs.mkdirSync(folder, { recursive: true })
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 Common {
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(sourceCodeSha256, functions)
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 sourceFile = this.getSourceFile(path, [])
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(provider: NodeProvider, address: string, group: number): Promise<ContractState> {
350
- const state = await provider.contracts.getContractsAddressState(address, { group: group })
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
- return JSON.stringify(
356
- {
357
- sourceCodeSha256: this.sourceCodeSha256,
358
- bytecode: this.bytecode,
359
- codeHash: this.codeHash,
360
- fieldsSig: this.fieldsSig,
361
- eventsSig: this.eventsSig,
362
- functions: this.functions
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 provider.contracts.postContractsTestContract(apiParams)
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
- provider: NodeProvider,
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
- provider: NodeProvider,
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 = await Contract.fromCodeHash(state.codeHash)
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: await Contract.getFieldsSig(state),
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 = await Contract.fromCodeHash(codeHash!)
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 Common {
687
+ export class Script extends Artifact {
599
688
  readonly bytecodeTemplate: string
600
689
  readonly fieldsSig: FieldsSig
601
690
 
602
- constructor(sourceCodeSha256: string, bytecodeTemplate: string, fieldsSig: FieldsSig, functions: FunctionSig[]) {
603
- super(sourceCodeSha256, functions)
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 checkCodeType(fileName: string, contractStr: string): void {
609
- const scriptMatches = contractStr.match(this.scriptRegex)
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.sourceCodeSha256, artifact.bytecodeTemplate, artifact.fieldsSig, artifact.functions)
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 sourceFile = this.getSourceFile(path, [])
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
- return JSON.stringify(
678
- {
679
- sourceCodeSha256: this.sourceCodeSha256,
680
- bytecodeTemplate: this.bytecodeTemplate,
681
- fieldsSig: this.fieldsSig,
682
- functions: this.functions
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> {