如何通过solc编译solidity编写的以太坊智能合约
曲水流觞113
发表于 2022-11-6 23:42:53
124
0
0
% j# t- m/ M/ v" q8 O
solidity编写的以太坊智能合约可通过命令行编译工具solc来进行编译,成为以太坊虚拟机中的代码。solc编译后最终部署到链上形成我们所见到的各种智能合约。" n$ b% n% S" c
6 _* D+ ~) U: Z* A6 |% l3 }) b' r* y+ h
作为一个solidity命令行编译工具,我们来看看官网都怎么说solc。
8 N) {& i; ?, o9 n l
solc的安装很简单:
npminstall-gsolc" O7 \0 V# U4 F! h3 n
//或者
& c9 t- L j0 X
npminstall-gsolc-cli) J! _7 n& Q+ d. P' [$ S
//或者5 |6 [0 \4 \2 z/ t6 H( [
% ~" O( `# p5 Y# a
sudoapt-getinstallsolc
( v4 Y- s. h' R& k+ J0 U% ^$ n( H
安装完成后我们来看,solc--help,solc--help命令显示所有的solc命令选项。编译器可以生成各种输出,比如最终的二进制合约文件、语法树的汇编或者需要预计的要花费的gas等。solc--binsourceFile.sol,可以编译后输出一个名为sourceFile.sol的智能合约文件。如果你想从solc获得更丰富的一些输出变量,你可以使用solc-ooutputDirectory--bin--ast--asmsourceFile.sol。
8 h4 D) @ c4 ~! a
你在部署以太坊智能合约之前可以用solc--optimize--binsourceFile.sol优化一下。默认情况下solc编译器会帮你优化200次。你也可以设置--runs=1,这样就按照最小化的方式进行编译,如果你希望多次交易不太在乎成本,那你可以设置成你想要的次数:)。( Q& T5 N1 \4 \0 o0 _# E. w/ v6 Z. R$ X
命令行编译器会自动读取需要导入的文件,也可以通过使用prefix=path来指定路径,例如:7 R, Q% B( g9 U" K5 L, T
* |2 y6 R+ p$ P" |$ R% {
solcgithub.com/ethereum/dapp-bin/=/usr/local/lib/dapp-bin/=/usr/local/lib/fallbackfile.sol2 s( d; w) u1 s. d. }' Y
这样编译器就会从指定目录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来切换。
如果智能合约使用了libraries,你会注意到字节码包含了__LibraryName______的子字符串。您可以使用solc作为链接器,这意味着它将在这些点为您插入库地址。; m( b& }& E8 T# M) t8 i- m s
可以通过添加库--libraries"Math:0x12345678901234567890Heap:0xabcdef0123456"到您的命令,以提供每个库的地址,或者使用文件中的说明字符串(每行一个库),并使用--librariesfileName运行solc。
$ q$ l+ q: B6 v" H( S
如果用选项--link调用Solc,则所有输入文件都被解释为未链接的二进制文件(HEX编码),在上面给出的__LibraryName____格式中,将其链接到适当地址(如果从stdin读取输入,则将其写入stdout)。在这种情况下,除了库外,所有选项都被忽略(包括-o)。9 A0 @" E, \! j% N
如果用--standard-json调用SOLC,它就将标准的JSON输入(如下所述),并返回JSON输出。8 H% s s$ }) Z0 O2 ^( }, G
#solc编译器输入输出JSON描述9 F0 K" ^. m; a/ `3 w. W
. N2 Q! ^: K5 S% O% R8 E
这些JSON格式通过编译器API使用,可以通过SOLC获得。内容都是可以修改的,一些对象是可选的(如前所述),其目的是向后兼容。
: D: c+ @. F/ V& v3 `
编译器的API需要一个JSON格式的输入,然后以JSON格式输出编译结果。
7 c* @' D1 R0 y) |( c8 p$ j
注意不允许注释。下面示例中的注释,是官网为了学习者更好的理解标注的。% ~/ M+ v0 B1 v; p5 U
输入格式说明:
{
& k2 Z% r: f* V
//Required:Sourcecodelanguage,suchas"Solidity","serpent","lll","assembly",etc.) L4 h/ G* Y, p* e0 G
' l# v) u" M3 O% K9 w
language:"Solidity",
+ V: Q4 K3 t) |* d2 z2 G! D0 Y
//Required# O$ ? ?9 t. l2 p" i
sources:
{# b# H/ R D& X
. G. x) O O% }% i; q
//Thekeysherearethe"global"namesofthesourcefiles,2 y T; @7 ]1 e* H1 g/ Z
//importscanuseotherfilesviaremappings(seebelow).
, W2 B& @3 \8 y$ R
"myFile.sol":: o" U% q [. e) v, V
{
* ^( E% o- U# i' ~- Q I. f
//Optional:keccak256hashofthesourcefile9 l* V ]6 |$ r6 E: R4 K
//ItisusedtoverifytheretrievedcontentifimportedviaURLs.
"keccak256":"0x123...",
C9 ?' X8 q, s# T* U4 J( _- Q
//Required(unless"content"isused,seebelow):URL(s)tothesourcefile.
0 S) }. d/ B1 L& o* ^1 N
//URL(s)shouldbeimportedinthisorderandtheresultcheckedagainstthe* k" v4 \ G0 l# s/ j
//keccak256hash(ifavailable).Ifthehashdoesn'tmatchornoneofthe
//URL(s)resultinsuccess,anerrorshouldberaised.
- x+ C- F! n# N7 @' E% U( }
"urls":
[
" i' a0 i3 R2 A: L9 G
"bzzr://56ab...",6 @8 F/ c# B" y `$ B H# n
; i, Y8 Z6 A; O( w7 t, D) @1 x g
"ipfs://Qma...",
$ Q5 z0 y3 t. C! z% c/ C
"file:///tmp/path/to/file.sol"2 F" X$ |8 ~+ H, |5 h5 ^5 \
]
" O9 |0 m7 y( W: h: O
},- |6 P ?. z, }% o
3 r) D1 u' m9 O5 F2 ?1 `! w
"mortal":
{: S4 z; p4 y: w; b; O1 v* p
//Optional:keccak256hashofthesourcefile
"keccak256":"0x234...",+ n5 W; T5 t0 Y8 d1 _. g
) X$ B G1 G6 m# L& }* U
//Required(unless"urls"isused):literalcontentsofthesourcefile! L5 k: F: Q3 W+ l' a
"content":"contractmortalisowned{functionkill(){if(msg.sender==owner)selfdestruct(owner);}}"
}7 V3 H) f- Z5 p# |- v
4 U1 c+ K+ W& ~$ ~1 ~
},
) J+ ?" |; f9 _
//Optional
settings:
# r8 r8 |& j9 S, v) ?
{* [: |/ Z" n; u0 A* f5 F
' o" ^; |: D# E( F' Q
//Optional:Sortedlistofremappings
* n; b* b5 `4 X
remappings:[":g/dir"],& C) U3 i( A4 M; i: y+ N# Y: x
//Optional:Optimizersettings
optimizer:{+ X7 j, ?$ q, ]: @* V# M+ K
. m$ @0 ~4 A* x; ^) g) _2 k* b4 d- A) F
//disabledbydefault/ {$ l2 f& d( n2 i9 ]3 n8 ~
1 M/ V( v) }. n- |( N( B3 Q1 B
enabled:true,
//Optimizeforhowmanytimesyouintendtorunthecode.6 _, w9 r1 i8 q2 x3 L) i6 n7 _" R. V' {5 ]
//Lowervalueswilloptimizemoreforinitialdeploymentcost,highervalueswilloptimizemoreforhigh-frequencyusage.7 b6 Q, M- r+ J. `1 A" O
runs:200! _# U! h3 Z Z$ u- p2 h H
: e$ K! H: ~8 c; C0 J$ m' b
},9 q- |7 k7 D4 n6 ?
evmVersion:"byzantium",//VersionoftheEVMtocompilefor.Affectstypecheckingandcodegeneration.Canbehomestead,tangerineWhistle,spuriousDragon,byzantiumorconstantinople" O: Q; }7 {6 z8 R0 o' d: N! v6 y+ m. @9 l
, |( |' n/ P3 T: B
//Metadatasettings(optional)
8 H/ R( f. i* ^, V9 I, O) Q
metadata:{
, J& P- ?# b3 S. A- o
//UseonlyliteralcontentandnotURLs(falsebydefault)
useLiteralContent:true
},
//Addressesofthelibraries.Ifnotalllibrariesaregivenhere,itcanresultinunlinkedobjectswhoseoutputdataisdifferent.. d- Y9 _9 d9 I" X# V. H$ i
" b' Y5 e- E" m1 Y
libraries:{
8 v6 F+ V& i) e% [1 n
//Thetoplevelkeyisthethenameofthesourcefilewherethelibraryisused.
//Ifremappingsareused,thissourcefileshouldmatchtheglobalpathafterremappingswereapplied.8 `: H0 P+ [% t* o5 d1 P
0 q; v/ B; F% _5 M% _3 n2 ~
//Ifthiskeyisanemptystring,thatreferstoagloballevel.* [5 J9 m( j1 R5 V- ^6 T
"myFile.sol":{
"MyLib":"0x123123..."* x; i/ F$ s, ]. F x- ]$ A' _
}
}0 v0 g% a# y/ l: _+ |7 Q' `
3 w' r; P# V% F7 X, L' j/ [
//Thefollowingcanbeusedtoselectdesiredoutputs.
, }, x0 S3 T8 E8 o- a6 W
//Ifthisfieldisomitted,thenthecompilerloadsanddoestypechecking,butwillnotgenerateanyoutputsapartfromerrors.8 ^ ?! G; \4 G
% u- l# s: j: L E6 B1 x, z
//Thefirstlevelkeyisthefilenameandthesecondisthecontractname,whereemptycontractnamereferstothefileitself,
1 ]) t* f: U. B7 H
//whilethestarreferstoallofthecontracts.
9 E. V# g0 i& P, @
//- O* z9 |; d5 @) Q# L7 k) K6 ?7 o
4 Q9 R/ y6 q% g
//Theavailableoutputtypesareasfollows:
//abi-ABI
9 e# u3 q3 c5 ^0 {& p
//ast-ASTofallsourcefiles0 V: f% g: Q$ v$ n, O N# ~
. O4 c2 M4 ]# u2 X
//legacyAST-legacyASTofallsourcefiles
5 Y( g( t* c) b. s% W3 X+ V
//devdoc-Developerdocumentation(natspec)
5 g/ a- L! d/ n) P, t+ K# t
//userdoc-Userdocumentation(natspec)+ |+ f+ x( H, S" T2 m& Y9 k) ~0 U
//metadata-Metadata# x- l" }0 `0 H8 U8 A
a ~/ I7 K1 i' V
//ir-Newassemblyformatbeforedesugaring
4 m3 f% b j5 g; R+ V* A- F
//evm.assembly-Newassemblyformatafterdesugaring* \; c% l6 ~/ R; f" [% Q4 S3 R
& p9 u& |0 T" @) e, i
//evm.legacyAssembly-Old-styleassemblyformatinJSON) h: B/ k: k% l6 P4 y
//evm.bytecode.object-Bytecodeobject
7 K8 J* E# g5 D j
//evm.bytecode.opcodes-Opcodeslist; C5 C' j' q7 ~+ X
, [8 R+ F' T+ M
//evm.bytecode.sourceMap-Sourcemapping(usefulfordebugging)
//evm.bytecode.linkReferences-Linkreferences(ifunlinkedobject)) m g) l1 \) ^* O4 h
8 x- n7 y! e3 {4 G. a, I
//evm.deployedBytecode*-Deployedbytecode(hasthesameoptionsasevm.bytecode)' @) t/ Q8 A7 i+ ^: s
//evm.methodIdentifiers-Thelistoffunctionhashes
//evm.gasEstimates-Functiongasestimates. p2 D3 Y% n& E% ~+ G
//ewasm.wast-eWASMS-expressionsformat(notsupportedatm)
//ewasm.wasm-eWASMbinaryformat(notsupportedatm)
//
//Notethatusingausing`evm`,`evm.bytecode`,`ewasm`,etc.willselectevery
% w( q; ?0 @* ^) M; x$ ?
//targetpartofthatoutput.Additionally,`*`canbeusedasawildcardtorequesteverything.
6 I' [7 j4 \. v5 ]
//
outputSelection:{0 l# N# V7 o( R$ W
& T; T: }8 F: e0 T! a. v2 h/ c
//Enablethemetadataandbytecodeoutputsofeverysinglecontract.
"*":{& n8 L$ d8 S( _1 A
9 l0 S! ]( n9 ?
"*":["metadata","evm.bytecode"]; A: i- e/ c) w
4 z. a9 p. w0 j& p) w4 \ c8 g8 u
},6 S! c( z2 g& F9 J% y2 _
* z7 o3 _+ @" d
//EnabletheabiandopcodesoutputofMyContractdefinedinfiledef.- `4 R( u: j A1 K. h1 P
, ?3 L1 \6 T. n) L3 z- d/ g
"def":{
5 c `" ` A) [; a! D
"MyContract":["abi","evm.bytecode.opcodes"]) ]5 R0 ^5 p9 a) s: R" ]. @ h$ B
},
//Enablethesourcemapoutputofeverysinglecontract.
"*":{
0 s' s' e2 |; V0 h
"*":["evm.bytecode.sourceMap"]% \- h) R: o# h$ L: e
},0 h6 k6 d7 q9 _, i) U2 T/ T
//EnablethelegacyASToutputofeverysinglefile.
"*":{0 D) h5 ?+ V' U3 T$ v
+ Y/ l' A2 l/ C6 i5 U/ X: j# Q) p
"":["legacyAST"]; [1 Y) u/ o* s# z7 A
+ @7 y& b" }; s: r9 D
}
2 d, z; t7 r/ B. {- ^- u o+ x
}
}
}2 A J5 S- P4 G- d/ b' p3 {3 \
$ P7 q7 Q4 z3 N3 b/ s$ t: O* ~
输出格式说明# H8 q! d$ B9 P9 i
5 B- c" n5 j( J$ I
{. }" I8 R' w: A" J
1 b: \- W0 `. P9 a" s T
//Optional:notpresentifnoerrors/warningswereencountered8 u# @1 E/ d# t3 w
" z3 y. G) i: C' B1 @3 V0 a9 y
errors:[7 a7 a u0 v" V$ O* d
{1 V( W: p) k5 d
0 ]4 c+ F/ ^' w% R, U7 W+ T+ x
//Optional:Locationwithinthesourcefile.6 Z! S8 K6 Q6 s2 ]: N6 m3 I, E' f
# [+ k# a% Z, ^, j: Z# O
sourceLocation:{: t' c3 A8 l* T% M( t
file:"sourceFile.sol",
4 ]) \- }3 S& V+ }& O
start:0,
end:1002 m8 H" z5 ^" F; I- d8 k n# C
2 q' b# y, t. m A) g; ~
],
//Mandatory:Errortype,suchas"TypeError","InternalCompilerError","Exception",etc.0 O" J% B6 t0 \7 r) S- f3 X
//Seebelowforcompletelistoftypes.
6 f3 b9 ]$ o# ^. b8 p6 y
type:"TypeError",3 `# B F9 Z' R# ^4 ^
//Mandatory:Componentwheretheerrororiginated,suchas"general","ewasm",etc.2 C" t7 R0 k+ _8 Z
" N' [/ w% L, X/ j6 G; m
component:"general",
4 u1 D3 j" y0 P) b+ f: j
//Mandatory("error"or"warning")& Q8 x: g8 k6 ~8 Q4 v1 o. T: S5 \
severity:"error",
% j) N% a: v8 h4 ~0 W
//Mandatory
message:"Invalidkeyword"
//Optional:themessageformattedwithsourcelocation
formattedMessage:"sourceFile.sol:100:Invalidkeyword"6 B# Z; w2 T& U0 h# W! X! |" D
0 X/ T/ f! c9 c% Z9 ?& J: |/ [4 k
}1 M9 O7 V% u8 l# p# m1 V
],8 A" Z4 T+ K# V$ W1 e
//Thiscontainsthefile-leveloutputs.Incanbelimited/filteredbytheoutputSelectionsettings.7 B: I+ ~! i* a# \7 c* ^3 N
8 r1 V) w% M, l. Q2 Q% @, e$ I
sources:{ }9 e4 f- p; ]( x
# V5 Z$ y: N: l' j+ _6 ?6 H9 q
"sourceFile.sol":{/ K( E$ n$ B# |+ z0 g+ |% a
//Identifier(usedinsourcemaps)
. G0 j3 j8 f$ s
id:1,* U+ O9 v5 e$ z$ P( Z9 G
//TheASTobject& a# [4 {6 X; n. M- R3 J$ A
1 _8 j0 p- p) M0 B: I# @) K* {6 p
ast:{},% m. T2 S6 `+ c2 N& n( ?
//ThelegacyASTobject
legacyAST:{}
! m) F+ S+ ~ i' |# X" E
}) X q8 F/ T8 U+ x/ A2 n! {3 V9 ^
1 `7 F% J! C) k/ j
},& `( @4 P) |6 @6 Y& ]! q# S
% n. C+ z4 ^. Z9 L) P) M
//Thiscontainsthecontract-leveloutputs.Itcanbelimited/filteredbytheoutputSelectionsettings.
contracts:{
9 N. D7 p1 v ~8 ?1 X
"sourceFile.sol":{
) z9 L' x+ |7 K' R- I7 {
//Ifthelanguageusedhasnocontractnames,thisfieldshouldequaltoanemptystring.
"ContractName":{
# w, b% t0 Z0 _" M; H
//TheEthereumContractABI.Ifempty,itisrepresentedasanemptyarray.
2 K8 C: S" J) W3 @
//Seehttps://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
abi:[],
9 R4 A' L4 t. L
//SeetheMetadataOutputdocumentation(serialisedJSONstring)
/ |# j' ]& \1 I5 K6 w
metadata:"{...}",$ a @3 V7 p& m9 h; J' O
//Userdocumentation(natspec)
# j) g2 |0 P) u3 d. m$ |
userdoc:{},/ h' N- [9 d" R$ R
0 z. u8 O1 x) T! Z+ J
//Developerdocumentation(natspec)
e: J* w9 u# |; X' c* |- A
devdoc:{},
. F2 \5 t4 n8 u# ]. I
//Intermediaterepresentation(string)
ir:"",
) S. L, W2 c% X
//EVM-relatedoutputs
/ d$ B- Z0 W( e. e8 l+ \) A
evm:{
//Assembly(string)
assembly:"",8 A' h8 r2 X0 I) u
//Old-styleassembly(object)
legacyAssembly:{},7 F* K7 a9 f4 o0 ]! U9 f' v
//Bytecodeandrelateddetails./ P) P4 V w' m, E' e. O
: s! g' k6 }1 \2 @" y0 P
bytecode:{
. }4 i" z+ D {
//Thebytecodeasahexstring.3 Z! u: F+ V! N; F a4 t+ O
object:"00fe",
//Opcodeslist(string)7 e4 @: v( @6 D. w; A0 x$ K- N* J5 S( t
opcodes:"",
" I9 D. S% {& K* Y7 f( G
//Thesourcemappingasastring.Seethesourcemappingdefinition.8 D' i: h) p- _5 Y' h
sourceMap:"",& D; t/ V% A, U. z. b
//Ifgiven,thisisanunlinkedobject.. [ ~0 e% K6 L# \! ^' `+ z
1 v( w% ^ H5 p# J3 h
linkReferences:{% p+ D3 W+ Z) e
"libraryFile.sol":{
) C. R, k& L1 R
//Byteoffsetsintothebytecode.Linkingreplacesthe20byteslocatedthere.
"Library1":[
" z; z: Q2 U( Q) U
{start:0,length:20},
{start:200,length:20}
0 L" G( b% ?5 w9 ]/ G2 L
]
}
/ b2 \$ T P1 M2 `" U4 G1 H O
}1 V2 N1 w/ ^. q
},
//Thesamelayoutasabove.$ g% F N' k9 r/ R) |
deployedBytecode:{},
6 }( ^3 r; U4 u: L
//Thelistoffunctionhashes
methodIdentifiers:{
"delegate(address)":"5c19a95c"2 c3 g! g; z- Y* K6 x8 ?
- `+ @2 _8 l4 t& L) G( \
},8 ]' N2 C, ]4 ~: ]( N2 \7 \
8 b0 m! B+ C1 N) v- L6 }; z1 G$ }9 c
//Functiongasestimates
gasEstimates:{
creation:{; G- v. v4 I* |* @" V
" u. q& h& @# A
codeDepositCost:"420000",& i0 ^( i1 G- b; U6 }4 Z
executionCost:"infinite",/ Z- p5 G; e5 Y
" J& C$ X) ]/ P% g$ S+ d5 G) o, q
totalCost:"infinite"# f3 h- g+ X& A
/ G6 v& {5 B3 g5 H! O# }8 C1 F
},
external:{
"delegate(address)":"25000"+ ?& q7 P" I3 p- v1 |& Y
},% [& A& V& @3 V8 l+ g: j5 W/ z
internal:{3 |3 l7 S. A- m$ v/ l: m4 \3 H
1 |7 d$ @, }0 z6 G2 r6 l, J6 q' r
"heavyLifting()":"infinite"
# l8 s" I6 W; x; ~2 t) X
}, M2 S9 g7 B8 A0 g
}
% b0 ?; N9 H6 ]% k* P
},
//eWASMrelatedoutputs$ n2 z( E( P% z8 ]! q: L1 X
1 E0 T* k2 Z3 y8 W& F
ewasm:{/ _5 c. b2 x$ \$ J
//S-expressionsformat
! w) c. g+ ~2 @8 ]9 O r5 R
wast:"",* @: }1 M4 [0 Q, s6 A5 M
& @* p8 ?1 R' X* `
//Binaryformat(hexstring)( ?& R4 Z! l3 u8 H7 k; _4 x h) h
6 o" E F( t* ]% M2 `; J, k9 T
wasm:""
}
; t4 T0 Y Y* d! u
}
}/ I5 ?: o r, c4 }: G3 |1 ~( t
}
# t0 e. _8 _2 m; p
}
. U" S* a! U" x$ r$ @7 D; d
错误类型说明:" J& n; F# ^, M* }
e: O. C0 Q4 U" L9 D7 {
( g9 E E5 f& e% t, h9 n$ J
JSONError:JSON错误,JSON输入不符合要求的格式,例如输入不是JSON对象,不支持语言,等等。
IOError:IO错误,IO和导入处理错误,如提供的源中的不可解析URL或hash不匹配。
ParserError:语法f分析错误,源代码不符合语言规则。/ e2 R0 M3 N( }2 p) K/ \
9 @: s9 w% C# I$ I6 Z% }
DocstringParsingError:文档解析错误,无法解析注释块中的NATSPEC标记。! D8 h; A; ~: c7 `, ?, s
SytRealError:语法错误,如continue在for循环之外使用。4 ^& Z, m7 m& y# Y* f, [
DeclarationError:声明错误,无效、不可解析或冲突的标识符名称。例如未找到标识符
4 V- ?7 T% Y1 o; a0 |& ~
TypeError:类型错误,如无效类型转换、无效赋值等。
7 A/ O8 v( m6 \9 l! j1 M8 J: e" Y
UnimplementedFeatureError:编译器不支持该特性,但希望在将来的版本中得到支持。/ J* k3 K, e1 _
InternalCompilerError:编译器中触发内部错误,这应该作为一个问题来反馈。
Exception:例外,编译过程中未知的故障,这应该作为一个问题反馈。
2 N/ z5 w+ Q: T8 I1 E
CompilerError:编译错误,编译器堆栈的使用无效,这应该作为一个问题来反馈。" j. {) i! o7 U" y* @1 I
& o3 z; `' h. a" `
FatalError:致命错误,这应该作为一个问题来反馈。
Warning:警告并没有停止编译,但如果可能的话,应该加以处理。
原文请访问:solc: B+ G; ^" ^' _0 z# R
! x1 n+ R% { }. @4 i4 ~1 U! q
如果你希望马上开始学习以太坊DApp开发,推荐访问一个在线教程:
2 u7 j$ h o6 j$ h
以太坊智能合约,主要介绍智能合约与dapp应用开发,适合入门。
以太坊开发,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
成为第一个吐槽的人