@alephium/web3 0.2.0-rc.3 → 0.2.0-rc.6
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 +3 -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 -231
- package/gitignore +0 -1
- package/package.json +2 -4
- package/src/api/api-alephium.ts +30 -0
- package/src/contract/contract.ts +385 -327
- package/templates/base/package.json +2 -2
- package/templates/react/package.json +2 -2
- package/test/contract.test.ts +38 -22
- package/test/events.test.ts +14 -9
- package/test/transaction.test.ts +5 -3
package/src/contract/contract.ts
CHANGED
|
@@ -26,163 +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
|
+
}
|
|
56
96
|
|
|
57
|
-
|
|
58
|
-
static readonly
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
}
|
|
111
|
+
|
|
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)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
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
|
+
}
|
|
80
201
|
}
|
|
81
202
|
|
|
82
|
-
|
|
83
|
-
|
|
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`)
|
|
208
|
+
}
|
|
209
|
+
Project.checkCompilerWarnings(contract.warnings, { ...DEFAULT_COMPILER_OPTIONS, ...compilerOptions })
|
|
210
|
+
return contract.artifact
|
|
84
211
|
}
|
|
85
212
|
|
|
86
|
-
static
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
|
|
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`)
|
|
91
218
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
default: {
|
|
105
|
-
dirs.push(part)
|
|
106
|
-
}
|
|
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}`)
|
|
152
346
|
}
|
|
347
|
+
if (matchNumber > 1) {
|
|
348
|
+
throw new Error(`Multiple definitions in file: ${contractPath}`)
|
|
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
|
-
) => Promise<T>,
|
|
166
|
-
errorOnWarnings: boolean
|
|
167
|
-
): Promise<T> {
|
|
168
|
-
Common.checkFileNameExtension(sourceFile.contractPath)
|
|
169
|
-
|
|
170
|
-
const contractStr = await loadContractStr(sourceFile, [])
|
|
171
|
-
const contractHash = cryptojs.SHA256(contractStr).toString()
|
|
172
|
-
const existingContract = this._getArtifactFromCache(contractHash)
|
|
173
|
-
if (typeof existingContract !== 'undefined') {
|
|
174
|
-
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)
|
|
175
388
|
} else {
|
|
176
|
-
|
|
389
|
+
Project.currentProject = await Project.loadArtifacts(
|
|
390
|
+
provider,
|
|
391
|
+
sourceFiles,
|
|
392
|
+
projectArtifact,
|
|
393
|
+
contractsRootPath,
|
|
394
|
+
artifactsRootPath
|
|
395
|
+
)
|
|
177
396
|
}
|
|
178
397
|
}
|
|
398
|
+
}
|
|
179
399
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
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
|
|
186
405
|
}
|
|
187
406
|
|
|
188
407
|
abstract buildByteCodeToDeploy(initialFields?: Fields): string
|
|
@@ -198,109 +417,31 @@ export abstract class Common {
|
|
|
198
417
|
usingAssetsInContractFunctions(): string[] {
|
|
199
418
|
return this.functions.filter((func) => func.useAssetsInContract).map((func) => func.name)
|
|
200
419
|
}
|
|
201
|
-
|
|
202
|
-
protected static checkCompilerWarnings(compiled: { warnings: string[] }, errorOnWarnings: boolean): void {
|
|
203
|
-
if (compiled.warnings.length !== 0) {
|
|
204
|
-
const prefixPerWarning = ' - '
|
|
205
|
-
const warningString = prefixPerWarning + compiled.warnings.join('\n' + prefixPerWarning)
|
|
206
|
-
const output = 'Compilation warnings:\n' + warningString + '\n'
|
|
207
|
-
if (errorOnWarnings) {
|
|
208
|
-
throw new Error(output)
|
|
209
|
-
} else {
|
|
210
|
-
console.log(output)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
420
|
}
|
|
215
421
|
|
|
216
|
-
export class Contract extends
|
|
422
|
+
export class Contract extends Artifact {
|
|
217
423
|
readonly bytecode: string
|
|
218
424
|
readonly codeHash: string
|
|
219
425
|
readonly fieldsSig: FieldsSig
|
|
220
426
|
readonly eventsSig: EventSig[]
|
|
221
427
|
|
|
222
428
|
constructor(
|
|
223
|
-
sourceCodeSha256: string,
|
|
224
429
|
bytecode: string,
|
|
225
430
|
codeHash: string,
|
|
226
431
|
fieldsSig: FieldsSig,
|
|
227
432
|
eventsSig: EventSig[],
|
|
228
433
|
functions: FunctionSig[]
|
|
229
434
|
) {
|
|
230
|
-
super(
|
|
435
|
+
super(functions)
|
|
231
436
|
this.bytecode = bytecode
|
|
232
437
|
this.codeHash = codeHash
|
|
233
438
|
this.fieldsSig = fieldsSig
|
|
234
439
|
this.eventsSig = eventsSig
|
|
235
440
|
}
|
|
236
441
|
|
|
237
|
-
static checkCodeType(fileName: string, contractStr: string): void {
|
|
238
|
-
const interfaceMatches = contractStr.match(Contract.interfaceRegex)
|
|
239
|
-
const contractMatches = contractStr.match(Contract.contractRegex)
|
|
240
|
-
if (interfaceMatches === null && contractMatches === null) {
|
|
241
|
-
throw new Error(`No contract found in: ${fileName}`)
|
|
242
|
-
}
|
|
243
|
-
if (interfaceMatches && contractMatches) {
|
|
244
|
-
throw new Error(`Multiple contracts and interfaces in: ${fileName}`)
|
|
245
|
-
}
|
|
246
|
-
if (interfaceMatches === null) {
|
|
247
|
-
if (contractMatches !== null && contractMatches.length > 1) {
|
|
248
|
-
throw new Error(`Multiple contracts in: ${fileName}`)
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (contractMatches === null) {
|
|
252
|
-
if (interfaceMatches !== null && interfaceMatches.length > 1) {
|
|
253
|
-
throw new Error(`Multiple interfaces in: ${fileName}`)
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
private static async loadContractStr(sourceFile: SourceFile): Promise<string> {
|
|
259
|
-
return Common._loadContractStr(sourceFile, [], (code) => Contract.checkCodeType(sourceFile.contractPath, code))
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
static async fromSource(provider: NodeProvider, path: string, errorOnWarnings = true): Promise<Contract> {
|
|
263
|
-
if (!fs.existsSync(Common._artifactsFolder())) {
|
|
264
|
-
fs.mkdirSync(Common._artifactsFolder(), { recursive: true })
|
|
265
|
-
}
|
|
266
|
-
const sourceFile = this.getSourceFile(path, [])
|
|
267
|
-
const contract = await Common._from(
|
|
268
|
-
provider,
|
|
269
|
-
sourceFile,
|
|
270
|
-
(sourceFile) => Contract.loadContractStr(sourceFile),
|
|
271
|
-
Contract.compile,
|
|
272
|
-
errorOnWarnings
|
|
273
|
-
)
|
|
274
|
-
this._putArtifactToCache(contract)
|
|
275
|
-
return contract
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
private static async compile(
|
|
279
|
-
provider: NodeProvider,
|
|
280
|
-
sourceFile: SourceFile,
|
|
281
|
-
contractStr: string,
|
|
282
|
-
contractHash: string,
|
|
283
|
-
errorOnWarnings: boolean
|
|
284
|
-
): Promise<Contract> {
|
|
285
|
-
const compiled = await provider.contracts.postContractsCompileContract({ code: contractStr })
|
|
286
|
-
Common.checkCompilerWarnings(compiled, errorOnWarnings)
|
|
287
|
-
|
|
288
|
-
const artifact = new Contract(
|
|
289
|
-
contractHash,
|
|
290
|
-
compiled.bytecode,
|
|
291
|
-
compiled.codeHash,
|
|
292
|
-
compiled.fields,
|
|
293
|
-
compiled.events,
|
|
294
|
-
compiled.functions
|
|
295
|
-
)
|
|
296
|
-
await artifact._saveToFile(sourceFile)
|
|
297
|
-
return artifact
|
|
298
|
-
}
|
|
299
|
-
|
|
300
442
|
// TODO: safely parse json
|
|
301
443
|
static fromJson(artifact: any): Contract {
|
|
302
444
|
if (
|
|
303
|
-
artifact.sourceCodeSha256 == null ||
|
|
304
445
|
artifact.bytecode == null ||
|
|
305
446
|
artifact.codeHash == null ||
|
|
306
447
|
artifact.fieldsSig == null ||
|
|
@@ -310,44 +451,42 @@ export class Contract extends Common {
|
|
|
310
451
|
throw Error('The artifact JSON for contract is incomplete')
|
|
311
452
|
}
|
|
312
453
|
const contract = new Contract(
|
|
313
|
-
artifact.sourceCodeSha256,
|
|
314
454
|
artifact.bytecode,
|
|
315
455
|
artifact.codeHash,
|
|
316
456
|
artifact.fieldsSig,
|
|
317
457
|
artifact.eventsSig,
|
|
318
458
|
artifact.functions
|
|
319
459
|
)
|
|
320
|
-
this._putArtifactToCache(contract)
|
|
321
460
|
return contract
|
|
322
461
|
}
|
|
323
462
|
|
|
463
|
+
static fromCompileResult(result: CompileContractResult): Contract {
|
|
464
|
+
return new Contract(result.bytecode, result.codeHash, result.fields, result.events, result.functions)
|
|
465
|
+
}
|
|
466
|
+
|
|
324
467
|
// support both 'code.ral' and 'code.ral.json'
|
|
325
468
|
static async fromArtifactFile(path: string): Promise<Contract> {
|
|
326
|
-
const
|
|
327
|
-
const artifactPath = sourceFile.artifactPath
|
|
328
|
-
const content = await fsPromises.readFile(artifactPath)
|
|
469
|
+
const content = await fsPromises.readFile(path)
|
|
329
470
|
const artifact = JSON.parse(content.toString())
|
|
330
471
|
return Contract.fromJson(artifact)
|
|
331
472
|
}
|
|
332
473
|
|
|
333
|
-
async fetchState(
|
|
334
|
-
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
|
+
})
|
|
335
478
|
return this.fromApiContractState(state)
|
|
336
479
|
}
|
|
337
480
|
|
|
338
481
|
override toString(): string {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
},
|
|
348
|
-
null,
|
|
349
|
-
2
|
|
350
|
-
)
|
|
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)
|
|
351
490
|
}
|
|
352
491
|
|
|
353
492
|
toState(fields: Fields, asset: Asset, address?: string): ContractState {
|
|
@@ -370,14 +509,13 @@ export class Contract extends Common {
|
|
|
370
509
|
}
|
|
371
510
|
|
|
372
511
|
private async _test(
|
|
373
|
-
provider: NodeProvider,
|
|
374
512
|
funcName: string,
|
|
375
513
|
params: TestContractParams,
|
|
376
514
|
expectPublic: boolean,
|
|
377
515
|
accessType: string
|
|
378
516
|
): Promise<TestContractResult> {
|
|
379
517
|
const apiParams: node.TestContract = this.toTestContract(funcName, params)
|
|
380
|
-
const apiResult = await
|
|
518
|
+
const apiResult = await Project.currentProject.nodeProvider.contracts.postContractsTestContract(apiParams)
|
|
381
519
|
|
|
382
520
|
const methodIndex =
|
|
383
521
|
typeof params.testMethodIndex !== 'undefined' ? params.testMethodIndex : this.getMethodIndex(funcName)
|
|
@@ -390,20 +528,12 @@ export class Contract extends Common {
|
|
|
390
528
|
}
|
|
391
529
|
}
|
|
392
530
|
|
|
393
|
-
async testPublicMethod(
|
|
394
|
-
|
|
395
|
-
funcName: string,
|
|
396
|
-
params: TestContractParams
|
|
397
|
-
): Promise<TestContractResult> {
|
|
398
|
-
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')
|
|
399
533
|
}
|
|
400
534
|
|
|
401
|
-
async testPrivateMethod(
|
|
402
|
-
|
|
403
|
-
funcName: string,
|
|
404
|
-
params: TestContractParams
|
|
405
|
-
): Promise<TestContractResult> {
|
|
406
|
-
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')
|
|
407
537
|
}
|
|
408
538
|
|
|
409
539
|
toApiFields(fields?: Fields): node.Val[] {
|
|
@@ -449,33 +579,8 @@ export class Contract extends Common {
|
|
|
449
579
|
}
|
|
450
580
|
}
|
|
451
581
|
|
|
452
|
-
static async fromCodeHash(codeHash: string): Promise<Contract> {
|
|
453
|
-
const cached = this._getArtifactFromCache(codeHash)
|
|
454
|
-
if (typeof cached !== 'undefined') {
|
|
455
|
-
return cached as Contract
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const files = await fsPromises.readdir(Common._artifactsFolder())
|
|
459
|
-
for (const file of files) {
|
|
460
|
-
if (file.endsWith('.ral.json')) {
|
|
461
|
-
try {
|
|
462
|
-
const contract = await Contract.fromArtifactFile(file)
|
|
463
|
-
if (contract.codeHash === codeHash) {
|
|
464
|
-
return contract as Contract
|
|
465
|
-
}
|
|
466
|
-
} catch (_) {}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
throw new Error(`Unknown code with code hash: ${codeHash}`)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
static async getFieldsSig(state: node.ContractState): Promise<FieldsSig> {
|
|
474
|
-
return Contract.fromCodeHash(state.codeHash).then((contract) => contract.fieldsSig)
|
|
475
|
-
}
|
|
476
|
-
|
|
477
582
|
async fromApiContractState(state: node.ContractState): Promise<ContractState> {
|
|
478
|
-
const contract =
|
|
583
|
+
const contract = Project.currentProject.contractByCodeHash(state.codeHash)
|
|
479
584
|
return {
|
|
480
585
|
address: state.address,
|
|
481
586
|
contractId: binToHex(contractIdFromAddress(state.address)),
|
|
@@ -483,7 +588,7 @@ export class Contract extends Common {
|
|
|
483
588
|
initialStateHash: state.initialStateHash,
|
|
484
589
|
codeHash: state.codeHash,
|
|
485
590
|
fields: fromApiFields(state.fields, contract.fieldsSig),
|
|
486
|
-
fieldsSig:
|
|
591
|
+
fieldsSig: contract.fieldsSig,
|
|
487
592
|
asset: fromApiAsset(state.asset)
|
|
488
593
|
}
|
|
489
594
|
}
|
|
@@ -511,7 +616,7 @@ export class Contract extends Common {
|
|
|
511
616
|
} else if (event.eventIndex == -2) {
|
|
512
617
|
eventSig = this.ContractDestroyedEvent
|
|
513
618
|
} else {
|
|
514
|
-
const contract =
|
|
619
|
+
const contract = Project.currentProject.contractByCodeHash(codeHash!)
|
|
515
620
|
eventSig = contract.eventsSig[event.eventIndex]
|
|
516
621
|
}
|
|
517
622
|
|
|
@@ -579,88 +684,41 @@ export class Contract extends Common {
|
|
|
579
684
|
}
|
|
580
685
|
}
|
|
581
686
|
|
|
582
|
-
export class Script extends
|
|
687
|
+
export class Script extends Artifact {
|
|
583
688
|
readonly bytecodeTemplate: string
|
|
584
689
|
readonly fieldsSig: FieldsSig
|
|
585
690
|
|
|
586
|
-
constructor(
|
|
587
|
-
super(
|
|
691
|
+
constructor(bytecodeTemplate: string, fieldsSig: FieldsSig, functions: FunctionSig[]) {
|
|
692
|
+
super(functions)
|
|
588
693
|
this.bytecodeTemplate = bytecodeTemplate
|
|
589
694
|
this.fieldsSig = fieldsSig
|
|
590
695
|
}
|
|
591
696
|
|
|
592
|
-
static
|
|
593
|
-
|
|
594
|
-
if (scriptMatches === null) {
|
|
595
|
-
throw new Error(`No script found in: ${fileName}`)
|
|
596
|
-
} else if (scriptMatches.length > 1) {
|
|
597
|
-
throw new Error(`Multiple scripts in: ${fileName}`)
|
|
598
|
-
} else {
|
|
599
|
-
return
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
private static async loadContractStr(sourceFile: SourceFile): Promise<string> {
|
|
604
|
-
return Common._loadContractStr(sourceFile, [], (code) => Script.checkCodeType(sourceFile.contractPath, code))
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
static async fromSource(provider: NodeProvider, path: string, errorOnWarnings = true): Promise<Script> {
|
|
608
|
-
const sourceFile = this.getSourceFile(path, [])
|
|
609
|
-
return Common._from(
|
|
610
|
-
provider,
|
|
611
|
-
sourceFile,
|
|
612
|
-
(sourceFile) => Script.loadContractStr(sourceFile),
|
|
613
|
-
Script.compile,
|
|
614
|
-
errorOnWarnings
|
|
615
|
-
)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
private static async compile(
|
|
619
|
-
provider: NodeProvider,
|
|
620
|
-
sourceFile: SourceFile,
|
|
621
|
-
scriptStr: string,
|
|
622
|
-
contractHash: string,
|
|
623
|
-
errorOnWarnings = true
|
|
624
|
-
): Promise<Script> {
|
|
625
|
-
const compiled = await provider.contracts.postContractsCompileScript({ code: scriptStr })
|
|
626
|
-
Common.checkCompilerWarnings(compiled, errorOnWarnings)
|
|
627
|
-
const artifact = new Script(contractHash, compiled.bytecodeTemplate, compiled.fields, compiled.functions)
|
|
628
|
-
await artifact._saveToFile(sourceFile)
|
|
629
|
-
return artifact
|
|
697
|
+
static fromCompileResult(result: CompileScriptResult): Script {
|
|
698
|
+
return new Script(result.bytecodeTemplate, result.fields, result.functions)
|
|
630
699
|
}
|
|
631
700
|
|
|
632
701
|
// TODO: safely parse json
|
|
633
702
|
static fromJson(artifact: any): Script {
|
|
634
|
-
if (
|
|
635
|
-
artifact.sourceCodeSha256 == null ||
|
|
636
|
-
artifact.bytecodeTemplate == null ||
|
|
637
|
-
artifact.fieldsSig == null ||
|
|
638
|
-
artifact.functions == null
|
|
639
|
-
) {
|
|
703
|
+
if (artifact.bytecodeTemplate == null || artifact.fieldsSig == null || artifact.functions == null) {
|
|
640
704
|
throw Error('The artifact JSON for script is incomplete')
|
|
641
705
|
}
|
|
642
|
-
return new Script(artifact.
|
|
706
|
+
return new Script(artifact.bytecodeTemplate, artifact.fieldsSig, artifact.functions)
|
|
643
707
|
}
|
|
644
708
|
|
|
645
709
|
static async fromArtifactFile(path: string): Promise<Script> {
|
|
646
|
-
const
|
|
647
|
-
const artifactPath = sourceFile.artifactPath
|
|
648
|
-
const content = await fsPromises.readFile(artifactPath)
|
|
710
|
+
const content = await fsPromises.readFile(path)
|
|
649
711
|
const artifact = JSON.parse(content.toString())
|
|
650
712
|
return this.fromJson(artifact)
|
|
651
713
|
}
|
|
652
714
|
|
|
653
715
|
override toString(): string {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
},
|
|
661
|
-
null,
|
|
662
|
-
2
|
|
663
|
-
)
|
|
716
|
+
const object = {
|
|
717
|
+
bytecodeTemplate: this.bytecodeTemplate,
|
|
718
|
+
fieldsSig: this.fieldsSig,
|
|
719
|
+
functions: this.functions
|
|
720
|
+
}
|
|
721
|
+
return JSON.stringify(object, null, 2)
|
|
664
722
|
}
|
|
665
723
|
|
|
666
724
|
async paramsForDeployment(params: BuildExecuteScriptTx): Promise<SignExecuteScriptTxParams> {
|