MEMO 中间件为开发者、企业等提供一个安全、灵活、具有可组合性的数据网络。用户可灵活选择适合自己的底层存储系统。
中间件会提供存储单价查询、存储套餐购买服务,用户选择充值并购买套餐,获得存储空间,之后就可以上传下载文件,并且支持用户查询上传文件列表。
启动中间件服务,默认监听端口为8080; 本文档所使用的例子中,http监听端口设为8081,baseURL为http://localhost:8081;以下所有请求URL应根据实际情况进行更改。
登录前,需要根据地址获取challenge message
,并且必须通过Origin
字段设置domain
。
请求URL:http://localhost:8081/challenge?address={address}
请求方式:GET
返回参数:text信息(EIP-4361定义的格式)
请求示例:
注意事项:调用challenge接口时,需要在headers的Origin字段中指定域名,目前仅支持域名http://memo.io
,否则会返回错误。
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
500 | InternalError | We encountered an internal error, please try again. |
用户可使用eth账号进行登录。该登录方式需要用户先调用challenge接口获取信息,随后,用户利用私钥对该信息进行签名,签名方式在EIP-191中定义。注意:在调用challenge接口获取挑战信息后,需要在30s内完成登录,否则登录失败。
请求URL:http://localhost:8081/login
请求方式:POST
请求参数(JSON格式):
{ "message": "memo.io wants you to sign in with your Ethereum account:\n0xFD976F1F3dC6413Da5Fed05471eaBB01F4FaaC42\n\n\nURI: http://memo.io\nVersion: 1\nChain ID: 985\nNonce: 12c2ad59e12abbe224cf86741c4bf00a21432fb2673b29a694b72062385f9b5d\nIssued At: 2023-04-23T07:36:10Z", "signature": "0xd5c406dd9ca168cc0894788cd262c3e9bd2f5413f87654c8cb685a9d872b9ab151fe80930c8de4da6adeadf38714387bee65c949ba47efa3eb5ee1335b6cc79400" }
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
message | 请求签名信息 | string | 是 | 调用lens的challenge接口获取需要签名的text信息 |
signature | 签名 | string | 是 | text签名后的信息 |
获取签名信息的方式如下述代码所示:
package main import ( "crypto/ecdsa" "flag" "fmt" "io/ioutil" "log" "net/http" "time" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" ) func main() { secretKey := flag.String("sk", "", "the sk to signature") flag.Parse() privateKey, err := crypto.HexToECDSA(*secretKey) if err != nil { fmt.Println(err.Error()) return } publicKey := privateKey.Public() publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) if !ok { log.Fatal("cannot assert type: publicKey is not of type *ecdsa.PublicKey") } address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() // get MEMO-Middleware challenge message text, err := Challenge(address) if err != nil { log.Fatal(err) } fmt.Println("message:", text) // eip191-signature hash := crypto.Keccak256([]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(text), text))) signature, err := crypto.Sign(hash, privateKey) if err != nil { log.Fatal(err) } sig := hexutil.Encode(signature) fmt.Println("login sig:\n", sig) } func Challenge(address string) (string, error) { client := &http.Client{Timeout: time.Minute} // ip:port should be corresponding to that MEMO-Middleware server is listening url := "http://localhost:8081/challenge" req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } params := req.URL.Query() params.Add("address", address) req.URL.RawQuery = params.Encode() req.Header.Set("Origin", "https://memo.io") res, err := client.Do(req) if err != nil { return "", err } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { return "", err } if res.StatusCode != http.StatusOK { return "", fmt.Errorf("respond code[%d]: %s", res.StatusCode, string(body)) } return string(body), nil }
返回参数(JSON):
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoxLCJhdWQiOiJtZW1vLmlvIiwiZXhwIjoxNjc3NDkwMTgyLCJpYXQiOjE2Nzc0ODkyODIsImlzcyI6Im1lbW8uaW8iLCJzdWIiOiIweEU3RTlmMTJmOTlhRDE3ZDQ3ODZiOUIxMjQ3QzA5N2U2M2NlYUY4RGIifQ.F0asDvu3LH3ccK6LAztBGF1TTzGw7Stc9gBEzVicuE4", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoyLCJhdWQiOiJtZW1vLmlvIiwiZXhwIjoxNjc4MDk0MDgyLCJpYXQiOjE2Nzc0ODkyODIsImlzcyI6Im1lbW8uaW8iLCJzdWIiOiIweEU3RTlmMTJmOTlhRDE3ZDQ3ODZiOUIxMjQ3QzA5N2U2M2NlYUY4RGIifQ.PDxQ2orOlsES6fvkyR-xWc6M1yBY8RiFTcn8m5AGROc" }
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
accessToken | 认证令牌 | string | 是 | 15分钟内,持有该令牌可以免密认证 |
refreshToken | 刷新令牌 | string | 是 | 7天内,持有该令牌可以重新生成认证令牌 |
请求示例:
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
500 | InternalError | We encountered an internal error, please try again. |
401 | Authentication | There is an empty parameter;Can't parse message;Got wrong chain id; Got wrong domain; Got wrong nonce; Got wrong address; Got wrong signature |
使用lens账户登录,无需获取1.1中的挑战信息,但需要调用lens的challenge接口获取需要签名的text信息(EIP-4361定义的格式),同时利用EIP-191定义的签名方式对text信息进行签名,需在30s内发出登录请求。运行中间件服务时,将开启或关闭检查账户是否是Lens账户。
请求URL:http://localhost:8081/lens/login
请求方式:POST
请求参数(JSON格式):
{ "message":"\nmemo.io wants you to sign in with your Ethereum account:\n0x51632235cc673a788E02B30B9F16F7B1D300194C\n\nSign in with ethereum to lens\n\nURI: memo.io\nVersion: 1\nChain ID: 137\nNonce: bcb9b92754e2b900\nIssued At: 2023-03-14T07:26:05.501Z\n ", "signature":"0x..." }
参数名 | 变量 | 类型 | 必填 | 描述 |
---|---|---|---|---|
message | 请求签名信息 | string | 是 | 调用lens的challenge接口获取需要签名的text信息 |
signature | 签名 | string | 是 | text签名后的信息 |
调用lens接口,获取text信息的方式如下:
import( "context" "github.com/machinebox/graphql" ) type Challenge struct { Challenge struct { Text string } `graphql:"challenge(request: $request)"` } type ChallengeRequest struct { Address string `json:"address"` } func ChallengeRequest(address string) (string, error) { client := graphql.NewClient("https://api.lens.dev") req := graphql.NewRequest(` query Challenge($request:ChallengeRequest!) { challenge(request:$request) { text } }`) req.Var("request", ChallengeRequest{ Address: address }) req.Header.Set("Origin", "memo.io") var query Challenge if err := client.Run(context.Background(), req, &query); err != nil { return "", err } return query.Challenge.Text, nil }
使用EIP-191定义的方式签名可以借鉴1.2中的代码示例。
返回参数(JSON):
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoxLCJhdWQiOiJtZW1vLmlvIiwiZXhwIjoxNjc3NDkwMTgyLCJpYXQiOjE2Nzc0ODkyODIsImlzcyI6Im1lbW8uaW8iLCJzdWIiOiIweEU3RTlmMTJmOTlhRDE3ZDQ3ODZiOUIxMjQ3QzA5N2U2M2NlYUY4RGIifQ.F0asDvu3LH3ccK6LAztBGF1TTzGw7Stc9gBEzVicuE4", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoyLCJhdWQiOiJtZW1vLmlvIiwiZXhwIjoxNjc4MDk0MDgyLCJpYXQiOjE2Nzc0ODkyODIsImlzcyI6Im1lbW8uaW8iLCJzdWIiOiIweEU3RTlmMTJmOTlhRDE3ZDQ3ODZiOUIxMjQ3QzA5N2U2M2NlYUY4RGIifQ.PDxQ2orOlsES6fvkyR-xWc6M1yBY8RiFTcn8m5AGROc", "isRegistered": false }
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
accessToken | 认证令牌 | string | 是 | 15分钟内,持有该令牌可以免密认证 |
refreshToken | 刷新令牌 | string | 是 | 7天内,持有该令牌可以重新生成认证令牌 |
isRegistered | 是否注册过Lens | bool | 是 | 账户是否已经在Lens中注册 |
请求示例:
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
517 | Address | The address {address} is not registered on lens |
500 | InternalError | We encountered an internal error, please try again. |
401 | Authentication | There is an empty parameter; Got wrong domain; Got wrong chain id; Got wrong address/signature; |
accessToken的有效期为15分钟;refreshToken的有效期为7天,当accesToken过期后,需要根据refreshToken刷新accessToken进行免密认证登录。
请求URL:http://localhost:8081/refresh
请求方式:GET
请求头信息:
参数名 | 变量 | 类型 | 必填 | 描述 |
---|---|---|---|---|
Authorization | "Bearer 刷新令牌" | string | 是 | 上述登录请求返回的refreshToken |
返回参数(JSON):
参数名 | 变量 | 类型【长度限制】 | 描述 |
---|---|---|---|
accessToken | 认证令牌 | string | 有效期过后重新生成的认证令牌 |
请求示例:
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
401 | Unauthorized | Illegal refresh token |
用户登录后,可上传文件。MEFS采用对象存储,文件默认上传至与登录账户地址同名的bucket中,账户上传文件时,若还未创建同名bucket,中间件则会自动帮用户创建同名bucket.
上传文件受用户的充值金额及存储空间限制,用户可先查询存储单价和存储套餐,进行充值,从而得到存储空间和余额。
请求URL:
选择上传至mefs:http://localhost:8081/mefs/
选择上传至ipfs:http://localhost:8081/ipfs/
请求方式:POST
请求头信息:
参数名 | 变量 |
---|---|
Content-Type | multipart/form-data |
Authorization | "Bearer 登录验证产生的access token" |
请求参数:
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
file | 待上传的文件 | File | 是 | 注意选择File格式 |
返回参数(JSON):
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
cid | 上传文件CID | string | 是 | 文件唯一标识符 |
返回例子:
Response:Status 200 { "cid": "bafybeie2ph7iokrckc5iy6xu7npa4xqst6ez5ewb7j2igeilxjaw6sd2qi" }
请求示例:
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
401 | Authentication | Token is Null; Invalid token payload; Invalid token; |
500 | InternalError | We encountered an internal error, please try again. |
518 | Storage | storage not support |
用户可以根据文件的cid下载相应的文件。
请求URL:
http://ip:port/mefs/$cid;
http://ip:port/ipfs/$cid;
选择从mefs下载:http://localhost:8081/mefs/bafkreifzwcj6vkozz6brwutpxl3hqneran4y5vtvirnbrtw3l2m3jtlgq4
选择从ipfs下载:http://localhost:8081/ipfs/bafkreifzwcj6vkozz6brwutpxl3hqneran4y5vtvirnbrtw3l2m3jtlgq4
请求方式:GET
请求头信息:
无
请求参数:
无
返回参数(DataFromReader):
返回文件。
参数名 | 描述 | 值 |
---|---|---|
code | 状态码 | 200 |
contentLength | 文件大小 | |
contentType | 文件类型 | |
reader | io.Reader,文件传输缓冲区 |
请求示例:
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
500 | InternalError | We encountered an internal error, please try again. |
518 | Storage | storage not support |
517 | Address | address is null |
删除上传的文件。仅支持MEFS类型存储的删除功能。
请求URL:
http://ip:port/mefs/delete
请求方式:GET
请求头信息:
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
Authorization | "Bearer 登录验证产生的accessToken" | string | 是 | 若过期,可通过刷新accessToken来获得新的有效accessToken |
请求参数:
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
mid | 文件mid | string | 是 | 文件唯一标识符,上传文件时返回的字符串 |
返回参数(JSON):
参数名 | 类型【长度限制】 | 描述 |
---|---|---|
Status | string | 删除成功或失败 |
请求示例:
用户查询自己所上传的文件列表。
请求URL:
http://ip:port/mefs/listobjects
http://ip:port/ipfs/listobjects
请求方式:GET
将列出登录账户的文件列表。
请求头信息:
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
Authorization | "Bearer 登录验证产生的accessToken" | string | 是 | 若过期,可通过刷新accessToken来获得新的有效accessToken |
请求参数:
无
返回参数(JSON):
参数名 | 类型【长度限制】 | 描述 |
---|---|---|
Address | string | 账户以太坊钱包地址 |
Storage | string | mefs或者ipfs |
Object | struct | 文件列表 |
每个文件包含信息:
参数名 | 类型 | 描述 |
---|---|---|
Name | string | 文件名 |
Size | int64 | 文件大小 |
Cid | string | 文件cid |
ModTime | time | 文件修改时间 |
UserDefined | struct | 关于文件的其他一些信息 |
UserDefined结构体包含信息:
参数名 | 类型 | 描述 |
---|---|---|
encryption | string | 文件加密方式 |
etag | string | 文件ID模式(默认cid) |
请求示例:
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
401 | Authentication | Token is Null; Invalid token payload; Invalid token; |
516 | Storage | list object error %s |
518 | Storage | storage not support |
用户查询自己的存储余额。
请求URL:http://localhost:8081/account/balance
请求方式:GET
请求头信息:
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
Authorization | "Bearer 登录验证产生的accessToken" | string | 是 | 若过期,可通过刷新accessToken来获得新的有效accessToken |
请求参数:
无
返回参数(JSON):
参数名 | 类型【长度限制】 | 描述 |
---|---|---|
Address | string | 登录账户的以太坊钱包地址 |
Balance | string | 余额的最小单位数字表示 |
请求示例:
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
401 | Authentication | Token is Null; Invalid token payload; Invalid token; |
516 | Storage | make bucket error %s; |
518 | Storage | storage not support |
520 | Eth | rpc error |
用户查询自己的存储空间,包括已用、可用、免费空间,以及上传文件数。
请求URL:http://localhost:8081/account/getstorage?stype={stype}
请求方式:GET
请求头信息:
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
Authorization | "Bearer 登录验证产生的accessToken" | string | 是 | 若过期,可通过刷新accessToken来获得新的有效accessToken |
请求参数:
参数名 | 变量 | 描述 |
---|---|---|
stype | 存储类型 | 目前支持可查询的存储类型有mefs,ipfs,qiniu |
返回参数(JSON):
参数名 | 类型【长度限制】 | 描述 |
---|---|---|
Used | string | 已使用空间 |
Available | string | 可用空间 |
Free | string | 免费空间 |
Files | string | 文件数 |
请求示例:
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
401 | Authentication | Token is Null; Invalid token payload; Invalid token;Authentication Failed (InValid token type) |
查询各种存储方式的存储单价。
服务端暂未实现。
查询多种存储方式的存储套餐。
请求URL:http://localhost:8081/account/pkginfos
请求方式:GET
请求头信息:
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
Authorization | "Bearer 登录验证产生的accessToken" | string | 是 | 若过期,可通过刷新accessToken来获得新的有效accessToken |
返回参数(JSON):
参数名 | 变量 | 类型【长度限制】 | 描述 |
---|---|---|---|
Time | 存储时间(秒) | string | 该套餐包含的存储时长 |
Kind | 存储类型 | int | 该套餐的存储方式,目前包含mefs和ipfs |
Buysize | 购买空间大小 | string | 该套餐包含的存储空间 |
Amount | 价格 | string | 该套餐的价格 |
State | 状态 | int | 该套餐目前是否有效,1表示有效,0表示无效 |
请求示例:
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
401 | Authentication | Token is Null; Invalid token payload; Invalid token; |
请求URL:http://localhost:8081/account/buypkg
请求方式:GET
请求头信息:
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
Authorization | "Bearer 登录验证产生的accessToken" | string | 是 | 若过期,可通过刷新accessToken来获得新的有效accessToken |
请求参数:
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
amount | 金额 | string | 是 | 花费金额 |
pkgid | 套餐ID | string | 是 | 套餐id,根据‘套餐查询’获得 |
chainid | 链ID | string | 是 | 执行购买套餐的区块链id |
返回参数(JSON):
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
Status | 状态 | string | 是 | 购买成功或失败 |
请求示例:
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
401 | Authentication | Token is Null; Invalid token payload; Invalid token; |
请求URL:http://localhost:8081/account/getbuypkgs
请求方式:GET
请求头信息:
参数名 | 变量 | 类型【长度限制】 | 必填 | 描述 |
---|---|---|---|---|
Authorization | "Bearer 登录验证产生的accessToken" | string | 是 | 若过期,可通过刷新accessToken来获得新的有效accessToken |
请求参数:
无
返回参数(JSON):
参数名 | 变量 | 类型【长度限制】 | 描述 |
---|---|---|---|
Starttime | 开始时间 | string | 已购买套餐的开始服务时间 |
Endtime | 结束时间 | string | 已购买套餐的服务结束时间 |
Kind | 类型 | int | 0: MEFS, 1: IPFS |
Buysize | 购买大小 | int | 已购买套餐的存储空间大小 |
Amount | 购买金额 | int | 已购买套餐的消费金额 |
State | 状态 | int | 套餐状态 |
请求示例:
错误码:
HTTP状态码 | 错误码 | 错误描述 |
---|---|---|
401 | Authentication | Token is Null; Invalid token payload; Invalid token; |