如何通过solc编译solidity编写的以太坊智能合约
曲水流觞113
发表于 2022-11-6 23:42:53
139
0
0
$ D( ~% b, M8 N) N! i4 C3 d
solidity编写的以太坊智能合约可通过命令行编译工具solc来进行编译,成为以太坊虚拟机中的代码。solc编译后最终部署到链上形成我们所见到的各种智能合约。
作为一个solidity命令行编译工具,我们来看看官网都怎么说solc。1 z9 k3 s: o7 U, o; t7 \ H
7 T ]+ s- _" s" {9 T
solc的安装很简单:' V" M& p1 r; V3 o$ l6 }; x
npminstall-gsolc* A, J" v3 I5 b4 h3 w
( H, H$ e- B/ j
//或者
9 C2 k% y; i Y& n
npminstall-gsolc-cli
//或者$ o+ q y. R2 x- G$ Y8 m3 m7 G' m
sudoapt-getinstallsolc! K/ [% q; ]5 z; i1 O7 A4 y
) f/ A/ t4 K9 p. ]. w1 \
安装完成后我们来看,solc--help,solc--help命令显示所有的solc命令选项。编译器可以生成各种输出,比如最终的二进制合约文件、语法树的汇编或者需要预计的要花费的gas等。solc--binsourceFile.sol,可以编译后输出一个名为sourceFile.sol的智能合约文件。如果你想从solc获得更丰富的一些输出变量,你可以使用solc-ooutputDirectory--bin--ast--asmsourceFile.sol。
. x; D% l' U* B" J+ y
你在部署以太坊智能合约之前可以用solc--optimize--binsourceFile.sol优化一下。默认情况下solc编译器会帮你优化200次。你也可以设置--runs=1,这样就按照最小化的方式进行编译,如果你希望多次交易不太在乎成本,那你可以设置成你想要的次数:)。/ w( l2 @" @% }% B
5 Q6 j4 E: D3 f8 s [
命令行编译器会自动读取需要导入的文件,也可以通过使用prefix=path来指定路径,例如:
solcgithub.com/ethereum/dapp-bin/=/usr/local/lib/dapp-bin/=/usr/local/lib/fallbackfile.sol
这样编译器就会从指定目录github.com/ethereum/dapp-bin/下的/usr/local/lib/dapp-bin/目录开始搜索,如果没有找到文件,它将查看/usr/local/lib/fallback。solc将只读取你指定的这两个路径的,因此像import"/etc/passwd";必须要通过/=重新映射才起作用。如果有多个匹配,则选择具有最长公共前缀的进行匹配。
出于安全上的考虑,编译器限制了它可以访问的一些目录。在命令行中指定的源文件的路径(及其子目录)和命令行指定的路径外其他所有内容都会被拒绝。--allow-paths/sample/path,/another/sample/path来切换。2 E% N: X8 Z% q) Z }- D/ I( d
% `; i1 Y( B) R; X
如果智能合约使用了libraries,你会注意到字节码包含了__LibraryName______的子字符串。您可以使用solc作为链接器,这意味着它将在这些点为您插入库地址。
7 f F( h4 x- P5 q, A
可以通过添加库--libraries"Math:0x12345678901234567890Heap:0xabcdef0123456"到您的命令,以提供每个库的地址,或者使用文件中的说明字符串(每行一个库),并使用--librariesfileName运行solc。1 `. l3 e5 E5 C2 E( |' ]- }3 E1 U6 d
- e% o9 B# d. v9 A/ c
如果用选项--link调用Solc,则所有输入文件都被解释为未链接的二进制文件(HEX编码),在上面给出的__LibraryName____格式中,将其链接到适当地址(如果从stdin读取输入,则将其写入stdout)。在这种情况下,除了库外,所有选项都被忽略(包括-o)。( l4 b2 q* i3 c/ U1 k0 T
如果用--standard-json调用SOLC,它就将标准的JSON输入(如下所述),并返回JSON输出。0 E$ B7 l; m5 @$ G5 |
#solc编译器输入输出JSON描述# {2 u1 r+ \% C& u3 Z
这些JSON格式通过编译器API使用,可以通过SOLC获得。内容都是可以修改的,一些对象是可选的(如前所述),其目的是向后兼容。
编译器的API需要一个JSON格式的输入,然后以JSON格式输出编译结果。9 d/ J% }, U( I- B, r( `
注意不允许注释。下面示例中的注释,是官网为了学习者更好的理解标注的。& F* y7 w7 A& n6 J- @& n: x4 v
输入格式说明:) c6 q% I" ]6 _8 a& T
{
//Required:Sourcecodelanguage,suchas"Solidity","serpent","lll","assembly",etc.
language:"Solidity",
( b7 A x o, W( M/ K. A+ R
//Required
c6 \# C' l: ?2 K! h) S
sources:
$ K! J' q" I, g0 Y
{- E- }$ Z k/ _+ c
* s5 X0 ~4 A$ ~, @4 |# ~
//Thekeysherearethe"global"namesofthesourcefiles,
. P, A- `, m* c- N5 x, x, T' L$ R, F
//importscanuseotherfilesviaremappings(seebelow).
"myFile.sol":
3 q' [( J. |; Q$ F
{
0 _: T0 a+ x9 H
//Optional:keccak256hashofthesourcefile& u6 f( R% T& o+ ~. M7 s# x
z- D# i) B; j) R& U
//ItisusedtoverifytheretrievedcontentifimportedviaURLs.! E4 \0 n& Q& f6 e8 N
"keccak256":"0x123...",4 [" X7 z* r1 B; q
//Required(unless"content"isused,seebelow):URL(s)tothesourcefile.1 L+ c7 c# }& u9 t7 u( _) C) ?
; y- ^2 s6 ~- M+ \
//URL(s)shouldbeimportedinthisorderandtheresultcheckedagainstthe1 _9 R- @* h7 g, V
4 s( y. L3 j+ i- A
//keccak256hash(ifavailable).Ifthehashdoesn'tmatchornoneofthe5 h. o6 d$ b8 g% _
//URL(s)resultinsuccess,anerrorshouldberaised.6 |" X5 `$ i: J7 M: |% m8 {
"urls":7 p' W$ M$ D. f4 A
: s6 Q; t% R, c* k
[& | [; ]& R0 R v4 ~
( O3 f" R# D. n! X: ?1 \- P: w
"bzzr://56ab...",
"ipfs://Qma...",
"file:///tmp/path/to/file.sol"+ o( [% V. i$ I1 z5 Y& M+ d- h
]1 _6 V3 `* ~& e
" ?: v) R' h' G* k) `! ^
},2 G: f: M1 X, M
: B: N9 C8 ~3 I: K: B
"mortal":
% X. A! K! m/ j, d5 P; H z& }0 y% J
{
; D( B3 M: x: u' d0 t1 h
//Optional:keccak256hashofthesourcefile7 J# a' h" q! p w' }) K: B
' X7 y% K/ T/ Y" F1 u
"keccak256":"0x234...",
//Required(unless"urls"isused):literalcontentsofthesourcefile
"content":"contractmortalisowned{functionkill(){if(msg.sender==owner)selfdestruct(owner);}}"; G. r' `9 _+ n/ r& V& `( h3 L, A6 [
}% M5 W6 u) _- u
. l7 H6 h$ X j, n
},$ Y. A) K7 I* |+ y* h& d
$ F- c" J/ O7 i& l
//Optional3 j& l9 ^1 l" ]: M5 F& d: ?
) u: l' Q/ u e! [! b
settings:
{: r0 `1 W* ?8 a" z3 X
//Optional:Sortedlistofremappings
9 h$ K; C, j+ \9 A
remappings:[":g/dir"], t5 m: A/ Z1 p. P/ e4 E1 I
$ y: C) i% g5 |/ k- B1 |- a/ h8 l
//Optional:Optimizersettings
J2 M, N- L5 i. Z1 Q
optimizer:{
+ E' k1 Q. v w
//disabledbydefault
' i; [/ M# ~' d K7 w% B$ `2 s: N" u' t
enabled:true,
//Optimizeforhowmanytimesyouintendtorunthecode.
//Lowervalueswilloptimizemoreforinitialdeploymentcost,highervalueswilloptimizemoreforhigh-frequencyusage.+ i: ] ~: L% c: A3 |# j2 z
, v- I6 C7 u1 R- i" {% T3 F5 h
runs:200
7 b+ _, _4 D! N8 @
},
5 S j C9 ]$ G! N; R' t- ~5 r
evmVersion:"byzantium",//VersionoftheEVMtocompilefor.Affectstypecheckingandcodegeneration.Canbehomestead,tangerineWhistle,spuriousDragon,byzantiumorconstantinople w6 a0 ^+ Q. }1 O- D" R
//Metadatasettings(optional)
metadata:{
//UseonlyliteralcontentandnotURLs(falsebydefault)# b& h3 e9 ]4 d
useLiteralContent:true0 F* ~' S5 C0 K+ ] k& N* v$ c
},
+ R4 O& h2 A6 ^0 L: f1 P
//Addressesofthelibraries.Ifnotalllibrariesaregivenhere,itcanresultinunlinkedobjectswhoseoutputdataisdifferent.
libraries:{9 b" g% R' ? l( R1 w) r; }
//Thetoplevelkeyisthethenameofthesourcefilewherethelibraryisused.) e# `& d- t! p: Z r* g# I
( S* X0 k6 F8 e9 F; t
//Ifremappingsareused,thissourcefileshouldmatchtheglobalpathafterremappingswereapplied.* m& `& z% @' B# G( N( k. @4 s4 l
//Ifthiskeyisanemptystring,thatreferstoagloballevel.. B* i0 D/ F5 j: m- M# s7 J+ H
"myFile.sol":{" _9 H$ L+ Q0 V6 ^2 T
"MyLib":"0x123123..."& d0 c, K H' L0 P4 ]; c; `
4 [# \) B0 j- X: N
}
+ g5 q( T" k. u' T' ^" J
}6 R9 G1 n; \; i* Z
//Thefollowingcanbeusedtoselectdesiredoutputs.
. Z" B' l& Y, [% U2 v+ Z
//Ifthisfieldisomitted,thenthecompilerloadsanddoestypechecking,butwillnotgenerateanyoutputsapartfromerrors.+ @% {4 b8 W J0 h q0 c$ [, T
//Thefirstlevelkeyisthefilenameandthesecondisthecontractname,whereemptycontractnamereferstothefileitself,
//whilethestarreferstoallofthecontracts.
//
//Theavailableoutputtypesareasfollows:
//abi-ABI5 C n0 I Z+ s, ]; h* A- t: a7 }! C. h
- l% Q7 a" e6 w
//ast-ASTofallsourcefiles
/ V) G* U* p9 n+ k6 O; q: R# A
//legacyAST-legacyASTofallsourcefiles1 M% q' v+ c; |
/ i9 Y6 _" G& h
//devdoc-Developerdocumentation(natspec)
//userdoc-Userdocumentation(natspec)
2 `4 Z- t0 g! w M0 g* F, N
//metadata-Metadata
//ir-Newassemblyformatbeforedesugaring
2 P/ |7 ~0 H- |5 o8 e
//evm.assembly-Newassemblyformatafterdesugaring* l! c8 q2 |3 { _4 H7 g3 J
//evm.legacyAssembly-Old-styleassemblyformatinJSON
. e- ]' c: e0 R/ E- B& H2 B
//evm.bytecode.object-Bytecodeobject
//evm.bytecode.opcodes-Opcodeslist
//evm.bytecode.sourceMap-Sourcemapping(usefulfordebugging)! Y0 q6 h3 h+ J2 J! T4 V+ I3 H2 s
! u/ A7 Z5 S! k- {0 [. |
//evm.bytecode.linkReferences-Linkreferences(ifunlinkedobject)
) R. l- j* b5 Q
//evm.deployedBytecode*-Deployedbytecode(hasthesameoptionsasevm.bytecode) b! G! _; a" @% v) W9 W$ z. {
//evm.methodIdentifiers-Thelistoffunctionhashes8 b6 N' B b5 G% ^9 C
) g1 n8 Z; h' B) t* @ y& G
//evm.gasEstimates-Functiongasestimates
+ ?( d2 Q$ T9 q0 j0 B; Q
//ewasm.wast-eWASMS-expressionsformat(notsupportedatm)' z; F) d8 \8 Q% }
! m2 t+ U5 a7 M' F4 Q3 e
//ewasm.wasm-eWASMbinaryformat(notsupportedatm)
//3 `/ W" P, K$ O# G. J: {
, p. h7 Q5 `: D3 v. T: W
//Notethatusingausing`evm`,`evm.bytecode`,`ewasm`,etc.willselectevery
//targetpartofthatoutput.Additionally,`*`canbeusedasawildcardtorequesteverything.
4 u( j* W; F! c" @ R
//
outputSelection:{
; z) f6 j" T4 _4 O3 d
//Enablethemetadataandbytecodeoutputsofeverysinglecontract.
- W( _/ {0 L; A {( k
"*":{0 F. S! w8 t" W% @5 U3 K$ d1 g
"*":["metadata","evm.bytecode"]0 K! J, p) |: \' M# {6 V1 B
},
//EnabletheabiandopcodesoutputofMyContractdefinedinfiledef.
) |7 t# E, h& k! l6 d- k5 b
"def":{9 Y9 T8 l- B2 _6 Y
$ W- p4 W# E5 i1 X
"MyContract":["abi","evm.bytecode.opcodes"]2 g3 o, k/ _1 y2 h
},
//Enablethesourcemapoutputofeverysinglecontract.
"*":{& V6 t8 M" @. J
4 [) Y$ g& Q1 O5 g# x
"*":["evm.bytecode.sourceMap"]
},5 n' Q8 ^) {. @
//EnablethelegacyASToutputofeverysinglefile.) J, G% ~, V. K) g$ U
1 T4 R# d' G* N# L4 Y" b
"*":{, K* e5 o$ @( ^9 i* N3 K
"":["legacyAST"]: Z J$ ?( O# \" H$ z
}' ~ K1 x& t7 G
}
} S( e* J* }0 n! s8 }; X" a
7 b, @2 x. r- Y7 w2 S& [
}
: z' o |, N) O8 E1 t" p9 B
输出格式说明/ m. k' M# M" W0 [
{
//Optional:notpresentifnoerrors/warningswereencountered
0 P. K8 C4 k8 \# Z1 }; @
errors:[( S% f! ^- u) |
8 R3 M1 j) Z( |
{# @% n& ` _3 Z- H r' N# m
//Optional:Locationwithinthesourcefile.
sourceLocation:{# _: F4 \ e; ~3 C
file:"sourceFile.sol",
start:0,! o" T# I/ C6 O2 u0 j. y8 q( J+ d
end:100
3 W; c9 K- t1 `8 ]% o
],
7 v4 `& Z' m- m5 c `' E; o4 u
//Mandatory:Errortype,suchas"TypeError","InternalCompilerError","Exception",etc.
$ ]2 L# K2 O5 R. B! m* e
//Seebelowforcompletelistoftypes.
s) R6 {3 D, b
type:"TypeError",
//Mandatory:Componentwheretheerrororiginated,suchas"general","ewasm",etc.
: ^# ^! B( \6 P* h
component:"general",2 {( M# s1 C G2 B
0 A$ |. i4 q5 W) o4 m2 q
//Mandatory("error"or"warning")9 G$ N" H! `9 I ]; Z: X
5 u# A. U# P8 g: g# G
severity:"error",& ^+ a/ p: P4 f2 Z' x' q7 m
//Mandatory
message:"Invalidkeyword"
+ @- \0 I! h* ^1 A& q
//Optional:themessageformattedwithsourcelocation
; U. s3 c H$ S3 V$ R' }. E/ ]3 Z
formattedMessage:"sourceFile.sol:100:Invalidkeyword"
}2 Z# q1 g* _* o! p; \
2 S. @& X6 g# z# N# C
],( u7 s$ |, H2 H' j; v! b0 r: \/ B
4 }* n$ z( v( D! k6 a" F
//Thiscontainsthefile-leveloutputs.Incanbelimited/filteredbytheoutputSelectionsettings.! T* _6 c3 ?9 c5 [. D
& n6 j5 ~9 ]/ }
sources:{$ z) @3 ^5 a: `0 m D) [8 F1 p
"sourceFile.sol":{
//Identifier(usedinsourcemaps)
L; B3 r4 W! C
id:1,
+ |9 c A/ r# J
//TheASTobject/ B1 Q6 P. G& q8 {8 _
* X) v8 ?. f0 _1 |$ M
ast:{},
//ThelegacyASTobject
% }# a9 A8 J a( v4 ^6 B! v
legacyAST:{} d8 D9 A$ e' ?7 r4 ?
0 t+ J5 N# _5 V: {& f1 A
}
P4 N h, C) I+ _) @: [
},
//Thiscontainsthecontract-leveloutputs.Itcanbelimited/filteredbytheoutputSelectionsettings.& a' _) l* e0 L1 v
) ^; V; F6 ` @0 \6 n4 t4 r
contracts:{! [4 ~9 A4 g+ w- h4 ?
: e0 d- n8 `; {! {0 r5 B. ^6 w
"sourceFile.sol":{
4 Z/ N1 _% B6 v/ |1 ]: j7 V* m
//Ifthelanguageusedhasnocontractnames,thisfieldshouldequaltoanemptystring.
"ContractName":{" ]1 K. z e/ g8 E3 S) L9 G: ~
/ p3 |, P t k, N6 b
//TheEthereumContractABI.Ifempty,itisrepresentedasanemptyarray.
+ _7 T% @* ]: h+ f' e A
//Seehttps://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI* e& T6 u1 ^( Q0 c' N
abi:[],: P$ h+ T6 H9 l* f7 _
! f1 B* g0 R y, t( W
//SeetheMetadataOutputdocumentation(serialisedJSONstring): j7 s# D0 Z2 p) N* F, ?
) J. ~9 N5 z3 z" Q$ w- e
metadata:"{...}",$ B( z/ {2 A, c+ w
//Userdocumentation(natspec); m1 U" Z' l6 O! Z9 e# D3 K
userdoc:{},4 x k% ~1 ~% ~& u0 {2 z2 v
6 a# u" {. c( ~, b; z$ X
//Developerdocumentation(natspec)4 [, @- O, ~0 ?5 O2 G, p
devdoc:{},
//Intermediaterepresentation(string)+ n! N- f4 |: k2 t1 I
ir:"",
% i6 I$ y3 w1 g
//EVM-relatedoutputs3 E8 |) N2 |. z) E# P3 [
evm:{
//Assembly(string)7 y" v" @, M" V/ _7 D8 g0 B
assembly:"",
& d1 {3 p3 X I3 T
//Old-styleassembly(object)
# l8 e6 \ ?+ U' {% Q5 f8 U; t) C4 A
legacyAssembly:{},1 j+ P9 V! g" X
4 T) n' d4 f1 j) h4 D
//Bytecodeandrelateddetails.3 l2 i- H' R9 A+ Q) d7 C
bytecode:{
//Thebytecodeasahexstring.. n' d# O* e+ {9 l* W; S
X6 D9 u* j" E }
object:"00fe",6 n7 J- _2 ?# g# b5 \
0 K) T, W! S% o0 e$ k
//Opcodeslist(string)
( Y6 Y, l) @3 C: c' q: x: {2 a5 b: L
opcodes:"",
9 a( J4 I6 V8 \
//Thesourcemappingasastring.Seethesourcemappingdefinition.0 ]- y/ U' x( t) U6 C
8 E6 Y' b1 p: [" d a0 d* S
sourceMap:"",
//Ifgiven,thisisanunlinkedobject.
linkReferences:{, I, B: R7 S; w( K& f) h, @7 h
1 ^. q- H- u3 ~$ A: B& v
"libraryFile.sol":{
8 p3 Q2 q8 s( |
//Byteoffsetsintothebytecode.Linkingreplacesthe20byteslocatedthere.
) `" a- _& H/ Q, @/ }( B
"Library1":[. f; r) \) o9 H6 h4 e, ^
& {' Z( z1 p ~; ^ i* |, c! k
{start:0,length:20},
, O8 |! M6 M9 x: n1 E7 j' J8 e
{start:200,length:20}- j& m# N8 I$ F
" s$ z5 i6 u9 d/ V+ L+ g+ K
]
2 x& h7 b, [+ z9 A0 d u% s, L& V
}0 \7 n- d @" h! L, b5 C
+ d" l8 m# i! g5 X2 ?! q
}: |) b5 s' v- l* k
, Q8 u' G- T( @# l
},- o3 S$ ]0 i% @; a
) L+ v2 D( S7 H# ]2 x* C$ ?! I
//Thesamelayoutasabove.
deployedBytecode:{},
//Thelistoffunctionhashes
methodIdentifiers:{
"delegate(address)":"5c19a95c"- ?% j- o4 n8 y
},& |! u" C- \# j& ]& n2 s
. c8 G# w: H6 M) r
//Functiongasestimates3 f, E! E* a& k: O
! Q: f5 |% V) X& P$ z" r& N
gasEstimates:{1 y' l5 d" K; v ]0 B
; @7 k) N; c+ \; T( L6 S
creation:{* `# ~& \6 l' C
# Y: ]; B. P' n- d
codeDepositCost:"420000",
executionCost:"infinite",5 m# m; X3 X; B$ G" e
" z6 u$ i- F- B5 ~9 q" z
totalCost:"infinite"
) k" [7 C! |! J R r
},: o/ ~+ _+ j3 s+ n& {
external:{1 m, F- o. s! S, _
"delegate(address)":"25000"! F' K6 s) X w; E# P$ q) d! r
/ J. p) V1 R2 a! C0 f x" N$ s
},5 Z9 U _$ ^/ d$ H0 M: h2 Y/ ]; R: T
internal:{
"heavyLifting()":"infinite"
3 f; G: Q) n w6 [& |
}
}- S( W" L2 H% r8 P0 p: }
7 ]7 ?5 N; ?% m B1 B4 K
},
# b. N9 z/ n3 j$ W1 R
//eWASMrelatedoutputs
ewasm:{
3 W3 E, u4 f5 O: O2 I
//S-expressionsformat
wast:"",
//Binaryformat(hexstring)
- s# G- J* D+ O+ T9 L S
wasm:""5 g# ^& Q* Q3 z+ a) Y9 k, r
, V' R" C+ w, g. o! S: f2 n
}. w9 O9 G, M9 L* P
9 \; k, p8 e6 b0 i) Q" V
}
}, z& X" C" w! Z# s* b
5 @' a9 S$ \6 I& Q! a
}
}
# z$ |, p' F% i6 R3 f
错误类型说明:( m) \! b3 L9 E4 v8 s3 D X
- \8 e" h2 t' F9 w
JSONError:JSON错误,JSON输入不符合要求的格式,例如输入不是JSON对象,不支持语言,等等。
IOError:IO错误,IO和导入处理错误,如提供的源中的不可解析URL或hash不匹配。! m# X' {6 u- M( S) _! m
ParserError:语法f分析错误,源代码不符合语言规则。
4 a& ?+ |+ O; Z% z& \+ F
DocstringParsingError:文档解析错误,无法解析注释块中的NATSPEC标记。8 f/ G' l* e) Y0 B: p# W
SytRealError:语法错误,如continue在for循环之外使用。" c" l" k9 `2 r( t5 x5 d% _3 S! \$ Q
- d' h6 l2 {) F' l
DeclarationError:声明错误,无效、不可解析或冲突的标识符名称。例如未找到标识符; R9 w% F4 s" s @
TypeError:类型错误,如无效类型转换、无效赋值等。
- \' j' U1 H! ]% z9 l
UnimplementedFeatureError:编译器不支持该特性,但希望在将来的版本中得到支持。$ Z; ?* Z! s4 s6 G3 F+ u
% B! K s% q- g5 x7 E
InternalCompilerError:编译器中触发内部错误,这应该作为一个问题来反馈。, l1 b9 @: T& n' i1 r' R; H5 r
Exception:例外,编译过程中未知的故障,这应该作为一个问题反馈。
: t9 B7 j( Q% I
CompilerError:编译错误,编译器堆栈的使用无效,这应该作为一个问题来反馈。! Q) o4 K( L7 V; M% m
FatalError:致命错误,这应该作为一个问题来反馈。% l/ p2 y3 H" B$ Q* ]
Warning:警告并没有停止编译,但如果可能的话,应该加以处理。; y1 E& U1 z+ a4 T6 {3 O; i. X+ f
1 y* j" U2 s& F* _
, d3 U6 n g% s2 `( l! l: I
原文请访问:solc; [2 k8 h5 j4 V1 g$ G
如果你希望马上开始学习以太坊DApp开发,推荐访问一个在线教程:
以太坊智能合约,主要介绍智能合约与dapp应用开发,适合入门。4 J4 R/ N2 I3 S- T. {5 y" t; {' W
以太坊开发,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
成为第一个吐槽的人