@alephium/web3 0.2.0-test.0 → 0.2.0-test.1

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.
Files changed (58) hide show
  1. package/contracts/add/add.ral +5 -8
  2. package/contracts/greeter/greeter.ral +3 -3
  3. package/contracts/greeter/greeter_interface.ral +1 -0
  4. package/contracts/greeter_main.ral +3 -5
  5. package/contracts/main.ral +0 -2
  6. package/contracts/sub/sub.ral +2 -1
  7. package/contracts/test/metadata.ral +18 -0
  8. package/contracts/test/warnings.ral +8 -0
  9. package/dist/alephium-web3.min.js +1 -1
  10. package/dist/alephium-web3.min.js.map +1 -1
  11. package/dist/scripts/create-project.js +2 -1
  12. package/dist/src/api/api-alephium.d.ts +37 -7
  13. package/dist/src/api/api-alephium.js +19 -3
  14. package/dist/src/api/api-explorer.d.ts +16 -0
  15. package/dist/src/api/api-explorer.js +26 -0
  16. package/dist/src/contract/contract.d.ts +86 -52
  17. package/dist/src/contract/contract.js +325 -218
  18. package/dist/src/global.d.ts +3 -0
  19. package/dist/src/global.js +38 -0
  20. package/dist/src/index.d.ts +1 -0
  21. package/dist/src/index.js +1 -0
  22. package/dist/src/signer/node-wallet.d.ts +1 -3
  23. package/dist/src/signer/node-wallet.js +2 -5
  24. package/dist/src/signer/signer.d.ts +1 -1
  25. package/dist/src/signer/signer.js +3 -2
  26. package/dist/src/test/index.d.ts +1 -2
  27. package/dist/src/test/index.js +4 -4
  28. package/dist/src/test/privatekey-wallet.d.ts +2 -3
  29. package/dist/src/test/privatekey-wallet.js +4 -4
  30. package/dist/src/utils/subscription.d.ts +0 -1
  31. package/dist/src/utils/subscription.js +2 -1
  32. package/dist/src/utils/utils.d.ts +2 -2
  33. package/dist/src/utils/utils.js +2 -2
  34. package/gitignore +0 -1
  35. package/package.json +3 -5
  36. package/scripts/create-project.ts +2 -1
  37. package/src/api/api-alephium.ts +57 -8
  38. package/src/api/api-explorer.ts +30 -0
  39. package/src/contract/contract.ts +430 -317
  40. package/src/contract/events.ts +2 -2
  41. package/src/contract/ralph.test.ts +4 -4
  42. package/src/global.ts +36 -0
  43. package/src/index.ts +1 -0
  44. package/src/signer/node-wallet.ts +2 -11
  45. package/src/signer/signer.ts +4 -3
  46. package/src/test/index.ts +2 -3
  47. package/src/test/privatekey-wallet.ts +4 -5
  48. package/src/transaction/status.ts +1 -1
  49. package/src/utils/subscription.ts +3 -3
  50. package/src/utils/utils.test.ts +1 -1
  51. package/src/utils/utils.ts +4 -4
  52. package/templates/base/package.json +2 -2
  53. package/templates/base/src/greeter.ts +8 -7
  54. package/templates/react/package.json +2 -2
  55. package/templates/react/src/App.tsx +2 -2
  56. package/test/contract.test.ts +60 -25
  57. package/test/events.test.ts +20 -17
  58. package/test/transaction.test.ts +10 -9
@@ -21,246 +21,438 @@ import * as cryptojs from 'crypto-js'
21
21
  import * as crypto from 'crypto'
22
22
  import fs from 'fs'
23
23
  import { promises as fsPromises } from 'fs'
24
- import { NodeProvider } from '../api'
25
- import { node } from '../api'
24
+ import { node, NodeProvider } from '../api'
26
25
  import { SignDeployContractTxParams, SignExecuteScriptTxParams, SignerWithNodeProvider } from '../signer'
27
26
  import * as ralph from './ralph'
28
27
  import { bs58, binToHex, contractIdFromAddress, assertType, Eq } from '../utils'
28
+ import { CompileContractResult, CompileScriptResult } from '../api/api-alephium'
29
+ import { getCurrentNodeProvider } from '../global'
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
+ }
29
41
 
30
- 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'
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
+ name: string | undefined
56
+
57
+ constructor(pattern: string, type: T) {
58
+ this.matcher = new RegExp(pattern, 'mg')
59
+ this.type = type
60
+ this.name = undefined
61
+ }
62
+
63
+ match(str: string): number {
64
+ const results = [...str.matchAll(this.matcher)]
65
+ if (results.length > 0) {
66
+ this.name = results[0][1]
45
67
  }
68
+ return results.length
46
69
  }
47
70
  }
48
71
 
49
- export abstract class Common {
50
- readonly sourceCodeSha256: string
51
- readonly functions: node.FunctionSig[]
52
-
53
- static readonly importRegex = new RegExp('^import "([^"/]+/(([^"]+)/)?)?[a-z][a-z_0-9]*.ral"', 'mg')
54
- static readonly contractRegex = new RegExp('^TxContract [A-Z][a-zA-Z0-9]*', 'mg')
55
- static readonly interfaceRegex = new RegExp('^Interface [A-Z][a-zA-Z0-9]* \\{', 'mg')
56
- static readonly scriptRegex = new RegExp('^TxScript [A-Z][a-zA-Z0-9]*', 'mg')
57
-
58
- private static _artifactCache: Map<string, Contract | Script> = new Map<string, Contract | Script>()
59
- static artifactCacheCapacity = 20
60
- protected static _getArtifactFromCache(codeHash: string): Contract | Script | undefined {
61
- return this._artifactCache.get(codeHash)
62
- }
63
- protected static _putArtifactToCache(contract: Contract): void {
64
- if (!this._artifactCache.has(contract.codeHash)) {
65
- if (this._artifactCache.size >= this.artifactCacheCapacity) {
66
- const keyToDelete = this._artifactCache.keys().next().value
67
- this._artifactCache.delete(keyToDelete)
72
+ class SourceFile {
73
+ type: SourceType
74
+ typeId: string
75
+ contractPath: string
76
+ sourceCode: string
77
+ sourceCodeHash: string
78
+
79
+ getArtifactPath(artifactsRootPath: string): string {
80
+ return artifactsRootPath + this.contractPath.slice(this.contractPath.indexOf('/')) + '.json'
81
+ }
82
+
83
+ constructor(type: SourceType, typeId: string, sourceCode: string, contractPath: string) {
84
+ this.type = type
85
+ this.typeId = typeId
86
+ this.sourceCode = sourceCode
87
+ this.sourceCodeHash = cryptojs.SHA256(sourceCode).toString()
88
+ this.contractPath = contractPath
89
+ }
90
+ }
91
+
92
+ class Compiled<T extends Artifact> {
93
+ sourceFile: SourceFile
94
+ artifact: T
95
+ warnings: string[]
96
+
97
+ constructor(sourceFile: SourceFile, artifact: T, warnings: string[]) {
98
+ this.sourceFile = sourceFile
99
+ this.artifact = artifact
100
+ this.warnings = warnings
101
+ }
102
+ }
103
+
104
+ class ProjectArtifact {
105
+ static readonly artifactFileName = '.project.json'
106
+
107
+ infos: Map<string, { sourceCodeHash: string; warnings: string[] }>
108
+
109
+ constructor(infos: Map<string, { sourceCodeHash: string; warnings: string[] }>) {
110
+ this.infos = infos
111
+ }
112
+
113
+ async saveToFile(rootPath: string): Promise<void> {
114
+ const filepath = rootPath + '/' + ProjectArtifact.artifactFileName
115
+ const content = JSON.stringify(Object.fromEntries(this.infos), null, 2)
116
+ return fsPromises.writeFile(filepath, content)
117
+ }
118
+
119
+ sourceHasChanged(files: SourceFile[]): boolean {
120
+ if (files.length !== this.infos.size) {
121
+ return true
122
+ }
123
+ for (const file of files) {
124
+ const info = this.infos.get(file.contractPath)
125
+ if (typeof info === 'undefined' || info.sourceCodeHash !== file.sourceCodeHash) {
126
+ return true
68
127
  }
69
- this._artifactCache.set(contract.codeHash, contract)
70
128
  }
129
+ return false
71
130
  }
72
131
 
73
- constructor(sourceCodeSha256: string, functions: node.FunctionSig[]) {
74
- this.sourceCodeSha256 = sourceCodeSha256
75
- this.functions = functions
132
+ static async from(rootPath: string): Promise<ProjectArtifact | undefined> {
133
+ const filepath = rootPath + '/' + ProjectArtifact.artifactFileName
134
+ if (!fs.existsSync(filepath)) {
135
+ return undefined
136
+ }
137
+ const content = await fsPromises.readFile(filepath)
138
+ const files = new Map(
139
+ Object.entries<{ sourceCodeHash: string; warnings: string[] }>(JSON.parse(content.toString()))
140
+ )
141
+ return new ProjectArtifact(files)
76
142
  }
143
+ }
77
144
 
78
- protected static _artifactsFolder(): string {
79
- return './artifacts/'
145
+ export class Project {
146
+ sourceFiles: SourceFile[]
147
+ contracts: Compiled<Contract>[]
148
+ scripts: Compiled<Script>[]
149
+
150
+ readonly contractsRootPath: string
151
+ readonly artifactsRootPath: string
152
+ readonly nodeProvider: NodeProvider
153
+
154
+ static currentProject: Project
155
+
156
+ static readonly abstractContractMatcher = new TypedMatcher<SourceType>(
157
+ '^Abstract Contract ([A-Z][a-zA-Z0-9]*)\\(*',
158
+ SourceType.AbstractContract
159
+ )
160
+ static readonly contractMatcher = new TypedMatcher('^Contract ([A-Z][a-zA-Z0-9]*)\\(*', SourceType.Contract)
161
+ static readonly interfaceMatcher = new TypedMatcher('^Interface ([A-Z][a-zA-Z0-9]*) \\{', SourceType.Interface)
162
+ static readonly scriptMatcher = new TypedMatcher('^TxScript ([A-Z][a-zA-Z0-9]*)( \\{*|\\(*)', SourceType.Script)
163
+ static readonly matchers = [
164
+ Project.abstractContractMatcher,
165
+ Project.contractMatcher,
166
+ Project.interfaceMatcher,
167
+ Project.scriptMatcher
168
+ ]
169
+
170
+ private constructor(
171
+ provider: NodeProvider,
172
+ contractsRootPath: string,
173
+ artifactsRootPath: string,
174
+ sourceFiles: SourceFile[],
175
+ contracts: Compiled<Contract>[],
176
+ scripts: Compiled<Script>[]
177
+ ) {
178
+ this.nodeProvider = provider
179
+ this.contractsRootPath = contractsRootPath
180
+ this.artifactsRootPath = artifactsRootPath
181
+ this.sourceFiles = sourceFiles
182
+ this.contracts = contracts
183
+ this.scripts = scripts
184
+ }
185
+
186
+ private getContractPath(path: string): string {
187
+ return path.startsWith(`./${this.contractsRootPath}`)
188
+ ? path.slice(2)
189
+ : path.startsWith(this.contractsRootPath)
190
+ ? path
191
+ : this.contractsRootPath + '/' + path
192
+ }
193
+
194
+ private static checkCompilerWarnings(warnings: string[], compilerOptions: CompilerOptions): void {
195
+ const remains = compilerOptions.ignoreUnusedConstantsWarnings
196
+ ? warnings.filter((s) => !s.includes('unused constants'))
197
+ : warnings
198
+ if (remains.length !== 0) {
199
+ const prefixPerWarning = ' - '
200
+ const warningString = prefixPerWarning + remains.join('\n' + prefixPerWarning)
201
+ const output = 'Compilation warnings:\n' + warningString + '\n'
202
+ if (compilerOptions.errorOnWarnings) {
203
+ throw new Error(output)
204
+ } else {
205
+ console.log(output)
206
+ }
207
+ }
80
208
  }
81
209
 
82
- static getSourceFile(path: string, _dirs: string[]): SourceFile {
83
- const parts = path.split('/')
84
- const dirs = Array.from(_dirs)
85
- if (parts.length === 1) {
86
- return new SourceFile(dirs, path)
210
+ static contract(path: string, compilerOptions?: Partial<CompilerOptions>): Contract {
211
+ const contractPath = Project.currentProject.getContractPath(path)
212
+ const contract = Project.currentProject.contracts.find((c) => c.sourceFile.contractPath === contractPath)
213
+ if (typeof contract === 'undefined') {
214
+ throw new Error(`Contract ${contractPath} does not exist`)
87
215
  }
88
- parts.slice(0, parts.length - 1).forEach((part) => {
89
- switch (part) {
90
- case '.': {
91
- break
92
- }
93
- case '..': {
94
- if (dirs.length === 0) {
95
- throw new Error('Invalid file path: ' + path)
96
- }
97
- dirs.pop()
98
- break
99
- }
100
- default: {
101
- dirs.push(part)
102
- }
216
+ Project.checkCompilerWarnings(contract.warnings, { ...DEFAULT_COMPILER_OPTIONS, ...compilerOptions })
217
+ return contract.artifact
218
+ }
219
+
220
+ static script(path: string, compilerOptions?: Partial<CompilerOptions>): Script {
221
+ const contractPath = Project.currentProject.getContractPath(path)
222
+ const script = Project.currentProject.scripts.find((c) => c.sourceFile.contractPath === contractPath)
223
+ if (typeof script === 'undefined') {
224
+ throw new Error(`Script ${contractPath} does not exist`)
225
+ }
226
+ Project.checkCompilerWarnings(script.warnings, { ...DEFAULT_COMPILER_OPTIONS, ...compilerOptions })
227
+ return script.artifact
228
+ }
229
+
230
+ private async saveArtifactsToFile(): Promise<void> {
231
+ const artifactsRootPath = this.artifactsRootPath
232
+ const saveToFile = async function (compiled: Compiled<Artifact>): Promise<void> {
233
+ const artifactPath = compiled.sourceFile.getArtifactPath(artifactsRootPath)
234
+ const folder = artifactPath.slice(0, artifactPath.lastIndexOf('/'))
235
+ if (!fs.existsSync(folder)) {
236
+ fs.mkdirSync(folder, { recursive: true })
103
237
  }
238
+ return fsPromises.writeFile(artifactPath, compiled.artifact.toString())
239
+ }
240
+ for (const contract of this.contracts) {
241
+ await saveToFile(contract)
242
+ }
243
+ for (const script of this.scripts) {
244
+ await saveToFile(script)
245
+ }
246
+ }
247
+
248
+ contractByCodeHash(codeHash: string): Contract {
249
+ const contract = this.contracts.find((c) => c.artifact.codeHash === codeHash)
250
+ if (typeof contract === 'undefined') {
251
+ throw new Error(`Unknown code with code hash: ${codeHash}`)
252
+ }
253
+ return contract.artifact
254
+ }
255
+
256
+ private async saveProjectArtifactToFile(): Promise<void> {
257
+ const files: Map<string, { sourceCodeHash: string; warnings: string[] }> = new Map()
258
+ this.contracts.forEach((c) => {
259
+ files.set(c.sourceFile.contractPath, {
260
+ sourceCodeHash: c.sourceFile.sourceCodeHash,
261
+ warnings: c.warnings
262
+ })
104
263
  })
105
- return new SourceFile(dirs, parts[parts.length - 1])
106
- }
107
-
108
- protected static async _handleImports(
109
- pathes: string[],
110
- contractStr: string,
111
- importsCache: string[]
112
- ): Promise<string> {
113
- const localImportsCache: string[] = []
114
- let result = contractStr.replace(Common.importRegex, (match) => {
115
- localImportsCache.push(match)
116
- return ''
264
+ this.scripts.forEach((s) => {
265
+ files.set(s.sourceFile.contractPath, {
266
+ sourceCodeHash: s.sourceFile.sourceCodeHash,
267
+ warnings: s.warnings
268
+ })
117
269
  })
118
- for (const myImport of localImportsCache) {
119
- const relativePath = myImport.slice(8, -1)
120
- const importSourceFile = this.getSourceFile(relativePath, pathes)
121
- if (!importsCache.includes(importSourceFile.contractPath)) {
122
- importsCache.push(importSourceFile.contractPath)
123
- const importContractStr = await Common._loadContractStr(importSourceFile, importsCache, (code) =>
124
- Contract.checkCodeType(importSourceFile.contractPath, code)
125
- )
126
- result = result.concat('\n', importContractStr)
127
- }
128
- }
129
- return result
270
+ const compiledSize = this.contracts.length + this.scripts.length
271
+ this.sourceFiles.slice(compiledSize).forEach((c) => {
272
+ files.set(c.contractPath, {
273
+ sourceCodeHash: c.sourceCodeHash,
274
+ warnings: []
275
+ })
276
+ })
277
+ const projectArtifact = new ProjectArtifact(files)
278
+ await projectArtifact.saveToFile(this.artifactsRootPath)
130
279
  }
131
280
 
132
- protected static async _loadContractStr(
133
- sourceFile: SourceFile,
134
- importsCache: string[],
135
- validate: (code: string) => void
136
- ): Promise<string> {
137
- const contractPath = sourceFile.contractPath
138
- const contractBuffer = await fsPromises.readFile(contractPath)
139
- const contractStr = contractBuffer.toString()
281
+ private static async compile(
282
+ provider: NodeProvider,
283
+ files: SourceFile[],
284
+ contractsRootPath: string,
285
+ artifactsRootPath: string
286
+ ): Promise<Project> {
287
+ const sourceStr = files.map((f) => f.sourceCode).join('\n')
288
+ const result = await provider.contracts.postContractsCompileProject({
289
+ code: sourceStr
290
+ })
291
+ const contracts: Compiled<Contract>[] = []
292
+ const scripts: Compiled<Script>[] = []
293
+ result.contracts.forEach((contractResult, index) => {
294
+ const sourceFile = files[`${index}`]
295
+ const contract = Contract.fromCompileResult(sourceFile.typeId, contractResult)
296
+ contracts.push(new Compiled(sourceFile, contract, contractResult.warnings))
297
+ })
298
+ result.scripts.forEach((scriptResult, index) => {
299
+ const sourceFile = files[index + contracts.length]
300
+ const script = Script.fromCompileResult(sourceFile.typeId, scriptResult)
301
+ scripts.push(new Compiled(sourceFile, script, scriptResult.warnings))
302
+ })
303
+ const project = new Project(provider, contractsRootPath, artifactsRootPath, files, contracts, scripts)
304
+ await project.saveArtifactsToFile()
305
+ await project.saveProjectArtifactToFile()
306
+ return project
307
+ }
140
308
 
141
- validate(contractStr)
142
- return Common._handleImports(sourceFile.dirs, contractStr, importsCache)
309
+ private static async loadArtifacts(
310
+ provider: NodeProvider,
311
+ files: SourceFile[],
312
+ projectArtifact: ProjectArtifact,
313
+ contractsRootPath: string,
314
+ artifactsRootPath: string
315
+ ): Promise<Project> {
316
+ try {
317
+ const contracts: Compiled<Contract>[] = []
318
+ const scripts: Compiled<Script>[] = []
319
+ for (const file of files) {
320
+ const info = projectArtifact.infos.get(file.contractPath)
321
+ if (typeof info === 'undefined') {
322
+ throw Error(`Unable to find project info for ${file.contractPath}, please rebuild the project`)
323
+ }
324
+ const warnings = info.warnings
325
+ const artifactPath = file.getArtifactPath(artifactsRootPath)
326
+ if (file.type === SourceType.Contract) {
327
+ const artifact = await Contract.fromArtifactFile(artifactPath)
328
+ contracts.push(new Compiled(file, artifact, warnings))
329
+ } else if (file.type === SourceType.Script) {
330
+ const artifact = await Script.fromArtifactFile(artifactPath)
331
+ scripts.push(new Compiled(file, artifact, warnings))
332
+ }
333
+ }
334
+ return new Project(provider, contractsRootPath, artifactsRootPath, files, contracts, scripts)
335
+ } catch (error) {
336
+ console.log(`Failed to load artifacts, error: ${error}, try to re-compile contracts...`)
337
+ return Project.compile(provider, files, contractsRootPath, artifactsRootPath)
338
+ }
143
339
  }
144
340
 
145
- static checkFileNameExtension(fileName: string): void {
146
- if (!fileName.endsWith('.ral')) {
147
- throw new Error('Smart contract file name should end with ".ral"')
341
+ private static async loadSourceFile(dirPath: string, filename: string): Promise<SourceFile> {
342
+ const contractPath = dirPath + '/' + filename
343
+ if (!filename.endsWith('.ral')) {
344
+ throw new Error(`Invalid filename: ${contractPath}, smart contract file name should end with ".ral"`)
345
+ }
346
+
347
+ const sourceBuffer = await fsPromises.readFile(contractPath)
348
+ const sourceStr = sourceBuffer.toString()
349
+ const results = this.matchers.map((m) => m.match(sourceStr))
350
+ const matchNumber = results.reduce((a, b) => a + b, 0)
351
+ if (matchNumber === 0) {
352
+ throw new Error(`No contract defined in file: ${contractPath}`)
353
+ }
354
+ if (matchNumber > 1) {
355
+ throw new Error(`Multiple definitions in file: ${contractPath}`)
356
+ }
357
+ const matcherIndex = results.indexOf(1)
358
+ const matcher = this.matchers[`${matcherIndex}`]
359
+ const type = matcher.type
360
+ if (matcher.name === undefined) {
361
+ throw new Error(`Invalid definition in file: ${contractPath}`)
362
+ }
363
+ return new SourceFile(type, matcher.name, sourceStr, contractPath)
364
+ }
365
+
366
+ private static async loadSourceFiles(contractsRootPath: string): Promise<SourceFile[]> {
367
+ const loadDir = async function (dirPath: string, results: SourceFile[]): Promise<void> {
368
+ const dirents = await fsPromises.readdir(dirPath, { withFileTypes: true })
369
+ for (const dirent of dirents) {
370
+ if (dirent.isFile()) {
371
+ const file = await Project.loadSourceFile(dirPath, dirent.name)
372
+ results.push(file)
373
+ } else {
374
+ const newPath = dirPath + '/' + dirent.name
375
+ await loadDir(newPath, results)
376
+ }
377
+ }
148
378
  }
379
+ const sourceFiles: SourceFile[] = []
380
+ await loadDir(contractsRootPath, sourceFiles)
381
+ const contractAndScriptSize = sourceFiles.filter(
382
+ (f) => f.type === SourceType.Contract || f.type === SourceType.Script
383
+ ).length
384
+ if (sourceFiles.length === 0 || contractAndScriptSize === 0) {
385
+ throw new Error('Project have no source files')
386
+ }
387
+ return sourceFiles.sort((a, b) => a.type - b.type)
149
388
  }
150
389
 
151
- protected static async _from<T extends { sourceCodeSha256: string }>(
152
- provider: NodeProvider,
153
- sourceFile: SourceFile,
154
- loadContractStr: (sourceFile: SourceFile, importsCache: string[]) => Promise<string>,
155
- compile: (provider: NodeProvider, sourceFile: SourceFile, contractStr: string, contractHash: string) => Promise<T>
156
- ): Promise<T> {
157
- Common.checkFileNameExtension(sourceFile.contractPath)
158
-
159
- const contractStr = await loadContractStr(sourceFile, [])
160
- const contractHash = cryptojs.SHA256(contractStr).toString()
161
- const existingContract = this._getArtifactFromCache(contractHash)
162
- if (typeof existingContract !== 'undefined') {
163
- return existingContract as unknown as T
390
+ static async build(contractsRootPath = 'contracts', artifactsRootPath = 'artifacts'): Promise<void> {
391
+ const provider = getCurrentNodeProvider()
392
+ const sourceFiles = await Project.loadSourceFiles(contractsRootPath)
393
+ const projectArtifact = await ProjectArtifact.from(artifactsRootPath)
394
+ if (typeof projectArtifact === 'undefined' || projectArtifact.sourceHasChanged(sourceFiles)) {
395
+ Project.currentProject = await Project.compile(provider, sourceFiles, contractsRootPath, artifactsRootPath)
164
396
  } else {
165
- return compile(provider, sourceFile, contractStr, contractHash)
397
+ Project.currentProject = await Project.loadArtifacts(
398
+ provider,
399
+ sourceFiles,
400
+ projectArtifact,
401
+ contractsRootPath,
402
+ artifactsRootPath
403
+ )
166
404
  }
167
405
  }
406
+ }
168
407
 
169
- protected _saveToFile(sourceFile: SourceFile): Promise<void> {
170
- const folder = Common._artifactsFolder() + sourceFile.dirPath
171
- if (!fs.existsSync(folder)) {
172
- fs.mkdirSync(folder, { recursive: true })
173
- }
174
- return fsPromises.writeFile(sourceFile.artifactPath, this.toString())
408
+ export abstract class Artifact {
409
+ readonly typeId: string
410
+ readonly functions: FunctionSig[]
411
+
412
+ constructor(typeId: string, functions: FunctionSig[]) {
413
+ this.typeId = typeId
414
+ this.functions = functions
175
415
  }
176
416
 
177
417
  abstract buildByteCodeToDeploy(initialFields?: Fields): string
418
+
419
+ publicFunctions(): string[] {
420
+ return this.functions.filter((func) => func.isPublic).map((func) => func.name)
421
+ }
422
+
423
+ usingPreapprovedAssetsFunctions(): string[] {
424
+ return this.functions.filter((func) => func.usePreapprovedAssets).map((func) => func.name)
425
+ }
426
+
427
+ usingAssetsInContractFunctions(): string[] {
428
+ return this.functions.filter((func) => func.useAssetsInContract).map((func) => func.name)
429
+ }
178
430
  }
179
431
 
180
- export class Contract extends Common {
432
+ export class Contract extends Artifact {
181
433
  readonly bytecode: string
182
434
  readonly codeHash: string
183
- readonly fieldsSig: node.FieldsSig
184
- readonly eventsSig: node.EventSig[]
435
+ readonly fieldsSig: FieldsSig
436
+ readonly eventsSig: EventSig[]
185
437
 
186
438
  constructor(
187
- sourceCodeSha256: string,
439
+ typeId: string,
188
440
  bytecode: string,
189
441
  codeHash: string,
190
- fieldsSig: node.FieldsSig,
191
- eventsSig: node.EventSig[],
192
- functions: node.FunctionSig[]
442
+ fieldsSig: FieldsSig,
443
+ eventsSig: EventSig[],
444
+ functions: FunctionSig[]
193
445
  ) {
194
- super(sourceCodeSha256, functions)
446
+ super(typeId, functions)
195
447
  this.bytecode = bytecode
196
448
  this.codeHash = codeHash
197
449
  this.fieldsSig = fieldsSig
198
450
  this.eventsSig = eventsSig
199
451
  }
200
452
 
201
- static checkCodeType(fileName: string, contractStr: string): void {
202
- const interfaceMatches = contractStr.match(Contract.interfaceRegex)
203
- const contractMatches = contractStr.match(Contract.contractRegex)
204
- if (interfaceMatches === null && contractMatches === null) {
205
- throw new Error(`No contract found in: ${fileName}`)
206
- }
207
- if (interfaceMatches && contractMatches) {
208
- throw new Error(`Multiple contracts and interfaces in: ${fileName}`)
209
- }
210
- if (interfaceMatches === null) {
211
- if (contractMatches !== null && contractMatches.length > 1) {
212
- throw new Error(`Multiple contracts in: ${fileName}`)
213
- }
214
- }
215
- if (contractMatches === null) {
216
- if (interfaceMatches !== null && interfaceMatches.length > 1) {
217
- throw new Error(`Multiple interfaces in: ${fileName}`)
218
- }
219
- }
220
- }
221
-
222
- private static async loadContractStr(sourceFile: SourceFile): Promise<string> {
223
- return Common._loadContractStr(sourceFile, [], (code) => Contract.checkCodeType(sourceFile.contractPath, code))
224
- }
225
-
226
- static async fromSource(provider: NodeProvider, path: string): Promise<Contract> {
227
- if (!fs.existsSync(Common._artifactsFolder())) {
228
- fs.mkdirSync(Common._artifactsFolder(), { recursive: true })
229
- }
230
- const sourceFile = this.getSourceFile(path, [])
231
- const contract = await Common._from(
232
- provider,
233
- sourceFile,
234
- (sourceFile) => Contract.loadContractStr(sourceFile),
235
- Contract.compile
236
- )
237
- this._putArtifactToCache(contract)
238
- return contract
239
- }
240
-
241
- private static async compile(
242
- provider: NodeProvider,
243
- sourceFile: SourceFile,
244
- contractStr: string,
245
- contractHash: string
246
- ): Promise<Contract> {
247
- const compiled = await provider.contracts.postContractsCompileContract({ code: contractStr })
248
- const artifact = new Contract(
249
- contractHash,
250
- compiled.bytecode,
251
- compiled.codeHash,
252
- compiled.fields,
253
- compiled.events,
254
- compiled.functions
255
- )
256
- await artifact._saveToFile(sourceFile)
257
- return artifact
258
- }
259
-
260
453
  // TODO: safely parse json
261
454
  static fromJson(artifact: any): Contract {
262
455
  if (
263
- artifact.sourceCodeSha256 == null ||
264
456
  artifact.bytecode == null ||
265
457
  artifact.codeHash == null ||
266
458
  artifact.fieldsSig == null ||
@@ -270,44 +462,44 @@ export class Contract extends Common {
270
462
  throw Error('The artifact JSON for contract is incomplete')
271
463
  }
272
464
  const contract = new Contract(
273
- artifact.sourceCodeSha256,
465
+ artifact.typeId,
274
466
  artifact.bytecode,
275
467
  artifact.codeHash,
276
468
  artifact.fieldsSig,
277
469
  artifact.eventsSig,
278
470
  artifact.functions
279
471
  )
280
- this._putArtifactToCache(contract)
281
472
  return contract
282
473
  }
283
474
 
475
+ static fromCompileResult(typeId: string, result: CompileContractResult): Contract {
476
+ return new Contract(typeId, result.bytecode, result.codeHash, result.fields, result.events, result.functions)
477
+ }
478
+
284
479
  // support both 'code.ral' and 'code.ral.json'
285
480
  static async fromArtifactFile(path: string): Promise<Contract> {
286
- const sourceFile = this.getSourceFile(path, [])
287
- const artifactPath = sourceFile.artifactPath
288
- const content = await fsPromises.readFile(artifactPath)
481
+ const content = await fsPromises.readFile(path)
289
482
  const artifact = JSON.parse(content.toString())
290
483
  return Contract.fromJson(artifact)
291
484
  }
292
485
 
293
- async fetchState(provider: NodeProvider, address: string, group: number): Promise<ContractState> {
294
- const state = await provider.contracts.getContractsAddressState(address, { group: group })
486
+ async fetchState(address: string, group: number): Promise<ContractState> {
487
+ const state = await Project.currentProject.nodeProvider.contracts.getContractsAddressState(address, {
488
+ group: group
489
+ })
295
490
  return this.fromApiContractState(state)
296
491
  }
297
492
 
298
493
  override toString(): string {
299
- return JSON.stringify(
300
- {
301
- sourceCodeSha256: this.sourceCodeSha256,
302
- bytecode: this.bytecode,
303
- codeHash: this.codeHash,
304
- fieldsSig: this.fieldsSig,
305
- eventsSig: this.eventsSig,
306
- functions: this.functions
307
- },
308
- null,
309
- 2
310
- )
494
+ const object = {
495
+ typeId: this.typeId,
496
+ bytecode: this.bytecode,
497
+ codeHash: this.codeHash,
498
+ fieldsSig: this.fieldsSig,
499
+ eventsSig: this.eventsSig,
500
+ functions: this.functions
501
+ }
502
+ return JSON.stringify(object, null, 2)
311
503
  }
312
504
 
313
505
  toState(fields: Fields, asset: Asset, address?: string): ContractState {
@@ -330,18 +522,17 @@ export class Contract extends Common {
330
522
  }
331
523
 
332
524
  private async _test(
333
- provider: NodeProvider,
334
525
  funcName: string,
335
526
  params: TestContractParams,
336
527
  expectPublic: boolean,
337
528
  accessType: string
338
529
  ): Promise<TestContractResult> {
339
530
  const apiParams: node.TestContract = this.toTestContract(funcName, params)
340
- const apiResult = await provider.contracts.postContractsTestContract(apiParams)
531
+ const apiResult = await Project.currentProject.nodeProvider.contracts.postContractsTestContract(apiParams)
341
532
 
342
533
  const methodIndex =
343
534
  typeof params.testMethodIndex !== 'undefined' ? params.testMethodIndex : this.getMethodIndex(funcName)
344
- const isPublic = this.functions[`${methodIndex}`].signature.indexOf('pub ') !== -1
535
+ const isPublic = this.functions[`${methodIndex}`].isPublic
345
536
  if (isPublic === expectPublic) {
346
537
  const result = await this.fromTestContractResult(methodIndex, apiResult)
347
538
  return result
@@ -350,20 +541,12 @@ export class Contract extends Common {
350
541
  }
351
542
  }
352
543
 
353
- async testPublicMethod(
354
- provider: NodeProvider,
355
- funcName: string,
356
- params: TestContractParams
357
- ): Promise<TestContractResult> {
358
- return this._test(provider, funcName, params, true, 'public')
544
+ async testPublicMethod(funcName: string, params: TestContractParams): Promise<TestContractResult> {
545
+ return this._test(funcName, params, true, 'public')
359
546
  }
360
547
 
361
- async testPrivateMethod(
362
- provider: NodeProvider,
363
- funcName: string,
364
- params: TestContractParams
365
- ): Promise<TestContractResult> {
366
- return this._test(provider, funcName, params, false, 'private')
548
+ async testPrivateMethod(funcName: string, params: TestContractParams): Promise<TestContractResult> {
549
+ return this._test(funcName, params, false, 'private')
367
550
  }
368
551
 
369
552
  toApiFields(fields?: Fields): node.Val[] {
@@ -409,33 +592,8 @@ export class Contract extends Common {
409
592
  }
410
593
  }
411
594
 
412
- static async fromCodeHash(codeHash: string): Promise<Contract> {
413
- const cached = this._getArtifactFromCache(codeHash)
414
- if (typeof cached !== 'undefined') {
415
- return cached as Contract
416
- }
417
-
418
- const files = await fsPromises.readdir(Common._artifactsFolder())
419
- for (const file of files) {
420
- if (file.endsWith('.ral.json')) {
421
- try {
422
- const contract = await Contract.fromArtifactFile(file)
423
- if (contract.codeHash === codeHash) {
424
- return contract as Contract
425
- }
426
- } catch (_) {}
427
- }
428
- }
429
-
430
- throw new Error(`Unknown code with code hash: ${codeHash}`)
431
- }
432
-
433
- static async getFieldsSig(state: node.ContractState): Promise<node.FieldsSig> {
434
- return Contract.fromCodeHash(state.codeHash).then((contract) => contract.fieldsSig)
435
- }
436
-
437
595
  async fromApiContractState(state: node.ContractState): Promise<ContractState> {
438
- const contract = await Contract.fromCodeHash(state.codeHash)
596
+ const contract = Project.currentProject.contractByCodeHash(state.codeHash)
439
597
  return {
440
598
  address: state.address,
441
599
  contractId: binToHex(contractIdFromAddress(state.address)),
@@ -443,21 +601,19 @@ export class Contract extends Common {
443
601
  initialStateHash: state.initialStateHash,
444
602
  codeHash: state.codeHash,
445
603
  fields: fromApiFields(state.fields, contract.fieldsSig),
446
- fieldsSig: await Contract.getFieldsSig(state),
604
+ fieldsSig: contract.fieldsSig,
447
605
  asset: fromApiAsset(state.asset)
448
606
  }
449
607
  }
450
608
 
451
- static ContractCreatedEvent: node.EventSig = {
609
+ static ContractCreatedEvent: EventSig = {
452
610
  name: 'ContractCreated',
453
- signature: 'event ContractCreated(address:Address)',
454
611
  fieldNames: ['address'],
455
612
  fieldTypes: ['Address']
456
613
  }
457
614
 
458
- static ContractDestroyedEvent: node.EventSig = {
615
+ static ContractDestroyedEvent: EventSig = {
459
616
  name: 'ContractDestroyed',
460
- signature: 'event ContractDestroyed(address:Address)',
461
617
  fieldNames: ['address'],
462
618
  fieldTypes: ['Address']
463
619
  }
@@ -466,14 +622,14 @@ export class Contract extends Common {
466
622
  event: node.ContractEventByTxId,
467
623
  codeHash: string | undefined
468
624
  ): Promise<ContractEventByTxId> {
469
- let eventSig: node.EventSig
625
+ let eventSig: EventSig
470
626
 
471
627
  if (event.eventIndex == -1) {
472
628
  eventSig = this.ContractCreatedEvent
473
629
  } else if (event.eventIndex == -2) {
474
630
  eventSig = this.ContractDestroyedEvent
475
631
  } else {
476
- const contract = await Contract.fromCodeHash(codeHash!)
632
+ const contract = Project.currentProject.contractByCodeHash(codeHash!)
477
633
  eventSig = contract.eventsSig[event.eventIndex]
478
634
  }
479
635
 
@@ -541,85 +697,42 @@ export class Contract extends Common {
541
697
  }
542
698
  }
543
699
 
544
- export class Script extends Common {
700
+ export class Script extends Artifact {
545
701
  readonly bytecodeTemplate: string
546
- readonly fieldsSig: node.FieldsSig
702
+ readonly fieldsSig: FieldsSig
547
703
 
548
- constructor(
549
- sourceCodeSha256: string,
550
- bytecodeTemplate: string,
551
- fieldsSig: node.FieldsSig,
552
- functions: node.FunctionSig[]
553
- ) {
554
- super(sourceCodeSha256, functions)
704
+ constructor(typeId: string, bytecodeTemplate: string, fieldsSig: FieldsSig, functions: FunctionSig[]) {
705
+ super(typeId, functions)
555
706
  this.bytecodeTemplate = bytecodeTemplate
556
707
  this.fieldsSig = fieldsSig
557
708
  }
558
709
 
559
- static checkCodeType(fileName: string, contractStr: string): void {
560
- const scriptMatches = contractStr.match(this.scriptRegex)
561
- if (scriptMatches === null) {
562
- throw new Error(`No script found in: ${fileName}`)
563
- } else if (scriptMatches.length > 1) {
564
- throw new Error(`Multiple scripts in: ${fileName}`)
565
- } else {
566
- return
567
- }
568
- }
569
-
570
- private static async loadContractStr(sourceFile: SourceFile): Promise<string> {
571
- return Common._loadContractStr(sourceFile, [], (code) => Script.checkCodeType(sourceFile.contractPath, code))
572
- }
573
-
574
- static async fromSource(provider: NodeProvider, path: string): Promise<Script> {
575
- const sourceFile = this.getSourceFile(path, [])
576
- return Common._from(provider, sourceFile, (sourceFile) => Script.loadContractStr(sourceFile), Script.compile)
577
- }
578
-
579
- private static async compile(
580
- provider: NodeProvider,
581
- sourceFile: SourceFile,
582
- scriptStr: string,
583
- contractHash: string
584
- ): Promise<Script> {
585
- const compiled = await provider.contracts.postContractsCompileScript({ code: scriptStr })
586
- const artifact = new Script(contractHash, compiled.bytecodeTemplate, compiled.fields, compiled.functions)
587
- await artifact._saveToFile(sourceFile)
588
- return artifact
710
+ static fromCompileResult(typeId: string, result: CompileScriptResult): Script {
711
+ return new Script(typeId, result.bytecodeTemplate, result.fields, result.functions)
589
712
  }
590
713
 
591
714
  // TODO: safely parse json
592
715
  static fromJson(artifact: any): Script {
593
- if (
594
- artifact.sourceCodeSha256 == null ||
595
- artifact.bytecodeTemplate == null ||
596
- artifact.fieldsSig == null ||
597
- artifact.functions == null
598
- ) {
716
+ if (artifact.bytecodeTemplate == null || artifact.fieldsSig == null || artifact.functions == null) {
599
717
  throw Error('The artifact JSON for script is incomplete')
600
718
  }
601
- return new Script(artifact.sourceCodeSha256, artifact.bytecodeTemplate, artifact.fieldsSig, artifact.functions)
719
+ return new Script(artifact.typeId, artifact.bytecodeTemplate, artifact.fieldsSig, artifact.functions)
602
720
  }
603
721
 
604
722
  static async fromArtifactFile(path: string): Promise<Script> {
605
- const sourceFile = this.getSourceFile(path, [])
606
- const artifactPath = sourceFile.artifactPath
607
- const content = await fsPromises.readFile(artifactPath)
723
+ const content = await fsPromises.readFile(path)
608
724
  const artifact = JSON.parse(content.toString())
609
725
  return this.fromJson(artifact)
610
726
  }
611
727
 
612
728
  override toString(): string {
613
- return JSON.stringify(
614
- {
615
- sourceCodeSha256: this.sourceCodeSha256,
616
- bytecodeTemplate: this.bytecodeTemplate,
617
- fieldsSig: this.fieldsSig,
618
- functions: this.functions
619
- },
620
- null,
621
- 2
622
- )
729
+ const object = {
730
+ typeId: this.typeId,
731
+ bytecodeTemplate: this.bytecodeTemplate,
732
+ fieldsSig: this.fieldsSig,
733
+ functions: this.functions
734
+ }
735
+ return JSON.stringify(object, null, 2)
623
736
  }
624
737
 
625
738
  async paramsForDeployment(params: BuildExecuteScriptTx): Promise<SignExecuteScriptTxParams> {
@@ -895,7 +1008,7 @@ export interface ContractState {
895
1008
  initialStateHash?: string
896
1009
  codeHash: string
897
1010
  fields: Fields
898
- fieldsSig: node.FieldsSig
1011
+ fieldsSig: FieldsSig
899
1012
  asset: Asset
900
1013
  }
901
1014
 
@@ -918,12 +1031,12 @@ function toApiContractState(state: ContractState): node.ContractState {
918
1031
  }
919
1032
  }
920
1033
 
921
- function toApiFields(fields: Fields, fieldsSig: node.FieldsSig): node.Val[] {
1034
+ function toApiFields(fields: Fields, fieldsSig: FieldsSig): node.Val[] {
922
1035
  return toApiVals(fields, fieldsSig.names, fieldsSig.types)
923
1036
  }
924
1037
 
925
- function toApiArgs(args: Arguments, funcSig: node.FunctionSig): node.Val[] {
926
- return toApiVals(args, funcSig.argNames, funcSig.argTypes)
1038
+ function toApiArgs(args: Arguments, funcSig: FunctionSig): node.Val[] {
1039
+ return toApiVals(args, funcSig.paramNames, funcSig.paramTypes)
927
1040
  }
928
1041
 
929
1042
  function toApiVals(fields: Fields, names: string[], types: string[]): node.Val[] {