提交 a6cd2e56 编写于 作者: D Daniel Larimer

fix tests after updating eoslib

上级 3c118627
......@@ -5,12 +5,11 @@ namespace TOKEN_NAME {
/// When storing accounts, check for empty balance and remove account
void storeAccount( AccountName account, const Account& a ) {
if( a.isEmpty() ) {
printi(account);
/// scope table key
Db::remove( account, N(account), N(account) );
/// value, scope
Accounts::remove( a, account );
} else {
/// scope table key value
Db::store( account, N(account), N(account), a );
/// value, scope
Accounts::store( a, account );
}
}
......@@ -34,7 +33,7 @@ using namespace currency;
extern "C" {
void init() {
storeAccount( N(currency), Account{ Tokens(1000ll*1000ll*1000ll) } );
storeAccount( N(currency), Account( CurrencyTokens(1000ll*1000ll*1000ll) ) );
}
/// The apply method implements the dispatch of events to this contract
......
......@@ -23,7 +23,15 @@ namespace TOKEN_NAME {
CurrencyTokens quantity;
};
/**
* @brief row in Account table stored within each scope
*/
struct Account {
Account( CurrencyTokens b = CurrencyTokens() ):balance(b){}
/**
* The key is constant because there is only one record per scope/currency/accounts
*/
const uint64_t key = N(account);
CurrencyTokens balance;
......
const char* currency_wast = R"=====(
(module
(type $FUNCSIG$vj (func (param i64)))
(type $FUNCSIG$ijjj (func (param i64 i64 i64) (result i32)))
(type $FUNCSIG$ijjii (func (param i64 i64 i32 i32) (result i32)))
(type $FUNCSIG$vj (func (param i64)))
(type $FUNCSIG$ijjjii (func (param i64 i64 i64 i32 i32) (result i32)))
(type $FUNCSIG$vii (func (param i32 i32)))
(type $FUNCSIG$ijjjjii (func (param i64 i64 i64 i64 i32 i32) (result i32)))
(type $FUNCSIG$iii (func (param i32 i32) (result i32)))
(import "env" "assert" (func $assert (param i32 i32)))
(import "env" "load_i64" (func $load_i64 (param i64 i64 i64 i64 i32 i32) (result i32)))
(import "env" "printi" (func $printi (param i64)))
(import "env" "load_i64" (func $load_i64 (param i64 i64 i64 i32 i32) (result i32)))
(import "env" "readMessage" (func $readMessage (param i32 i32) (result i32)))
(import "env" "remove_i64" (func $remove_i64 (param i64 i64 i64) (result i32)))
(import "env" "requireAuth" (func $requireAuth (param i64)))
(import "env" "requireNotice" (func $requireNotice (param i64)))
(import "env" "store_i64" (func $store_i64 (param i64 i64 i64 i32 i32) (result i32)))
(import "env" "store_i64" (func $store_i64 (param i64 i64 i32 i32) (result i32)))
(table 0 anyfunc)
(memory $0 1)
(data (i32.const 4) "p\04\00\00")
......@@ -27,47 +26,44 @@ const char* currency_wast = R"=====(
(func $_ZN8currency12storeAccountEyRKNS_7AccountE (param $0 i64) (param $1 i32)
(block $label$0
(br_if $label$0
(i64.eqz
(i64.load
(i64.eq
(i64.load offset=8
(get_local $1)
)
(i64.const 0)
)
)
(drop
(call $store_i64
(get_local $0)
(i64.const 21967113313)
(i64.const 21967113313)
(get_local $1)
(i32.const 8)
(i32.const 16)
)
)
(return)
)
(call $printi
(get_local $0)
)
(drop
(call $remove_i64
(get_local $0)
(i64.const 21967113313)
(i64.const 21967113313)
(i64.load
(get_local $1)
)
)
)
)
(func $_ZN8currency23apply_currency_transferERKNS_8TransferE (param $0 i32)
(local $1 i64)
(local $2 i64)
(local $3 i64)
(local $4 i32)
(local $2 i32)
(i32.store offset=4
(i32.const 0)
(tee_local $4
(tee_local $2
(i32.sub
(i32.load offset=4
(i32.const 0)
)
(i32.const 16)
(i32.const 32)
)
)
)
......@@ -89,34 +85,65 @@ const char* currency_wast = R"=====(
(get_local $0)
)
)
(set_local $1
(call $_ZN8currency10getAccountEy
(i64.store offset=16
(get_local $2)
(i64.const 21967113313)
)
(i64.store offset=24
(get_local $2)
(i64.const 0)
)
(drop
(call $load_i64
(i64.load
(get_local $0)
)
(i64.const 862690298531)
(i64.const 21967113313)
(i32.add
(get_local $2)
(i32.const 16)
)
(i32.const 16)
)
)
(set_local $2
(call $_ZN8currency10getAccountEy
(i64.store
(get_local $2)
(i64.const 21967113313)
)
(i64.store offset=8
(get_local $2)
(i64.const 0)
)
(drop
(call $load_i64
(i64.load offset=8
(get_local $0)
)
(i64.const 862690298531)
(i64.const 21967113313)
(get_local $2)
(i32.const 16)
)
)
(call $assert
(i64.ge_u
(get_local $1)
(i64.load offset=24
(get_local $2)
)
(i64.load offset=16
(get_local $0)
)
)
(i32.const 16)
)
(i64.store offset=8
(get_local $4)
(i64.store offset=24
(get_local $2)
(i64.sub
(get_local $1)
(tee_local $3
(i64.load offset=24
(get_local $2)
)
(tee_local $1
(i64.load offset=16
(get_local $0)
)
......@@ -126,88 +153,112 @@ const char* currency_wast = R"=====(
(call $assert
(i64.ge_u
(i64.add
(get_local $2)
(get_local $3)
(get_local $1)
(i64.load offset=8
(get_local $2)
)
)
(get_local $3)
(get_local $1)
)
(i32.const 64)
)
(i64.store
(get_local $4)
(i64.store offset=8
(get_local $2)
(i64.add
(get_local $2)
(i64.load offset=8
(get_local $2)
)
(i64.load offset=16
(get_local $0)
)
)
)
(call $_ZN8currency12storeAccountEyRKNS_7AccountE
(set_local $1
(i64.load
(get_local $0)
)
(i32.add
(get_local $4)
(i32.const 8)
)
)
(call $_ZN8currency12storeAccountEyRKNS_7AccountE
(i64.load offset=8
(get_local $0)
)
(get_local $4)
)
(i32.store offset=4
(i32.const 0)
(i32.add
(get_local $4)
(i32.const 16)
(block $label$0
(block $label$1
(br_if $label$1
(i64.eq
(i64.load offset=24
(get_local $2)
)
(i64.const 0)
)
)
(drop
(call $store_i64
(get_local $1)
(i64.const 21967113313)
(i32.add
(get_local $2)
(i32.const 16)
)
(i32.const 16)
)
)
(br $label$0)
)
)
)
(func $_ZN8currency10getAccountEy (param $0 i64) (result i64)
(local $1 i32)
(i32.store offset=4
(i32.const 0)
(tee_local $1
(i32.sub
(i32.load offset=4
(i32.const 0)
(drop
(call $remove_i64
(get_local $1)
(i64.const 21967113313)
(i64.load offset=16
(get_local $2)
)
(i32.const 16)
)
)
)
(i64.store offset=8
(get_local $1)
(i64.const 0)
)
(drop
(call $load_i64
(get_local $0)
(i64.const 862690298531)
(i64.const 21967113313)
(i64.const 21967113313)
(set_local $1
(i64.load
(i32.add
(get_local $1)
(get_local $0)
(i32.const 8)
)
(i32.const 8)
)
)
(set_local $0
(i64.load offset=8
(get_local $1)
(block $label$2
(block $label$3
(br_if $label$3
(i64.eq
(i64.load
(i32.add
(get_local $2)
(i32.const 8)
)
)
(i64.const 0)
)
)
(drop
(call $store_i64
(get_local $1)
(i64.const 21967113313)
(get_local $2)
(i32.const 16)
)
)
(br $label$2)
)
(drop
(call $remove_i64
(get_local $1)
(i64.const 21967113313)
(i64.load
(get_local $2)
)
)
)
)
(i32.store offset=4
(i32.const 0)
(i32.add
(get_local $1)
(i32.const 16)
(get_local $2)
(i32.const 32)
)
)
(get_local $0)
)
(func $init
(local $0 i32)
......@@ -226,11 +277,16 @@ const char* currency_wast = R"=====(
(get_local $0)
(i64.const 1000000000)
)
(call $_ZN8currency12storeAccountEyRKNS_7AccountE
(i64.const 862690298531)
(i32.add
(i64.store
(get_local $0)
(i64.const 21967113313)
)
(drop
(call $store_i64
(i64.const 862690298531)
(i64.const 21967113313)
(get_local $0)
(i32.const 8)
(i32.const 16)
)
)
(i32.store offset=4
......@@ -242,16 +298,15 @@ const char* currency_wast = R"=====(
)
)
(func $apply (param $0 i64) (param $1 i64)
(local $2 i64)
(local $3 i32)
(local $2 i32)
(i32.store offset=4
(i32.const 0)
(tee_local $3
(tee_local $2
(i32.sub
(i32.load offset=4
(i32.const 0)
)
(i32.const 48)
(i32.const 32)
)
)
)
......@@ -269,113 +324,30 @@ const char* currency_wast = R"=====(
)
)
(i64.store offset=24
(get_local $3)
(get_local $2)
(i64.const 0)
)
(drop
(call $readMessage
(i32.add
(get_local $3)
(get_local $2)
(i32.const 8)
)
(i32.const 24)
)
)
(set_local $0
(i64.load offset=8
(get_local $3)
)
)
(call $requireNotice
(i64.load offset=16
(get_local $3)
)
)
(call $requireNotice
(get_local $0)
)
(call $requireAuth
(i64.load offset=8
(get_local $3)
)
)
(set_local $0
(call $_ZN8currency10getAccountEy
(i64.load offset=8
(get_local $3)
)
)
)
(set_local $1
(call $_ZN8currency10getAccountEy
(i64.load offset=16
(get_local $3)
)
)
)
(call $assert
(i64.ge_u
(get_local $0)
(i64.load offset=24
(get_local $3)
)
)
(i32.const 16)
)
(i64.store offset=40
(get_local $3)
(i64.sub
(get_local $0)
(tee_local $2
(i64.load offset=24
(get_local $3)
)
)
)
)
(call $assert
(i64.ge_u
(i64.add
(get_local $2)
(get_local $1)
)
(get_local $2)
)
(i32.const 64)
)
(i64.store offset=32
(get_local $3)
(i64.add
(get_local $1)
(i64.load offset=24
(get_local $3)
)
)
)
(call $_ZN8currency12storeAccountEyRKNS_7AccountE
(i64.load offset=8
(get_local $3)
)
(call $_ZN8currency23apply_currency_transferERKNS_8TransferE
(i32.add
(get_local $3)
(i32.const 40)
)
)
(call $_ZN8currency12storeAccountEyRKNS_7AccountE
(i64.load offset=16
(get_local $3)
)
(i32.add
(get_local $3)
(i32.const 32)
(get_local $2)
(i32.const 8)
)
)
)
(i32.store offset=4
(i32.const 0)
(i32.add
(get_local $3)
(i32.const 48)
(get_local $2)
(i32.const 32)
)
)
)
......
......@@ -90,6 +90,8 @@ int32_t store_i64( AccountName scope, TableName table, const void* data, uint32_
* @return the number of bytes read or -1 if key was not found
*/
int32_t load_i64( AccountName scope, AccountName code, TableName table, void* data, uint32_t datalen );
int32_t front_i64( AccountName scope, AccountName code, TableName table, void* data, uint32_t datalen );
int32_t back_i64( AccountName scope, AccountName code, TableName table, void* data, uint32_t datalen );
/**
* @return 1 if a record was removed, and 0 if no record with key was found
......
......@@ -8,7 +8,6 @@ extern "C" {
/**
* @ingroup databaseCpp
*/
struct Db
{
......@@ -34,15 +33,15 @@ struct Db
}
template<typename T>
static int32_t store( Name scope, TableName table, uint64_t key, const T& value ) {
return store_i64( scope, table, key, &value, sizeof(value) );
static int32_t store( Name scope, TableName table, const T& value ) {
return store_i64( scope, table, &value, sizeof(value) );
}
static bool remove( Name scope, TableName table, uint64_t key ) {
return remove_i64( scope, table, key );
}
};
*/
template<int Primary, int Secondary>
......@@ -83,7 +82,7 @@ struct table_impl<sizeof(uint64_t),0> {
return back_i64( scope, code, table, data, len );
}
static bool remove( uint64_t scope, uint64_t table, const void* data ) {
return remove_i64( scope, table, data );
return remove_i64( scope, table, *((uint64_t*)data) );
}
static int32_t load_primary( uint64_t scope, uint64_t code, uint64_t table, void* data, uint32_t len ) {
return load_i64( scope, code, table, data, len );
......@@ -113,7 +112,7 @@ template<uint64_t scope, uint64_t code, uint64_t table, typename Record, typenam
struct Table {
private:
typedef table_impl<sizeof( PrimaryType ), sizeof( SecondaryType )> impl;
static_assert( sizeof(PrimaryType) + sizeof(SecondaryType) >= sizeof(Record), "invalid template parameters" );
static_assert( sizeof(PrimaryType) + sizeof(SecondaryType) <= sizeof(Record), "invalid template parameters" );
public:
typedef PrimaryType Primary;
......@@ -144,8 +143,8 @@ struct Table {
static bool upper_bound( const PrimaryType& p, Record& r ) {
return impl::upper_bound_primary( scope, code, table, &p &r, sizeof(Record) ) == sizeof(Record);
}
static bool remove( const Record& r ) {
return impl::remove( scope, table, &r );
static bool remove( const Record& r, uint64_t s = scope ) {
return impl::remove( s, table, &r );
}
};
......@@ -205,8 +204,8 @@ struct Table {
template<uint64_t scope, uint64_t code, uint64_t table, typename Record, typename PrimaryType>
struct Table<scope,code,table,Record,PrimaryType,void> {
private:
typedef table_impl<sizeof( PrimaryType )> impl;
static_assert( sizeof(PrimaryType) >= sizeof(Record), "invalid template parameters" );
typedef table_impl<sizeof( PrimaryType ),0> impl;
static_assert( sizeof(PrimaryType) <= sizeof(Record), "invalid template parameters" );
public:
typedef PrimaryType Primary;
......
file(GLOB SOURCE_FILES "*.cpp")
add_wast_target(exchange "${SOURCE_FILES}" "${CMAKE_SOURCE_DIR}/contracts" ${CMAKE_CURRENT_SOURCE_DIR})
\ No newline at end of file
add_wast_target(exchange "${SOURCE_FILES}" "${CMAKE_SOURCE_DIR}/contracts" ${CMAKE_CURRENT_SOURCE_DIR})
add_dependencies( exchange currency )
......@@ -43,11 +43,11 @@ namespace exchange {
inline void save( const Account& a ) {
if( a.isEmpty() ) {
print("remove");
Db::remove( N(exchange), N(account), a.owner );
Accounts::remove(a);
}
else {
print("store");
Db::store( N(exchange), N(account), a.owner, a );
Accounts::store(a);
}
}
......@@ -101,8 +101,8 @@ void apply_eos_transfer( const eos::Transfer& transfer ) {
void match( Bid& bid, Account& buyer, Ask& ask, Account& seller ) {
eos::Tokens ask_eos = ask.quantity * ask.price;
eos::Tokens fill_amount_eos = min<eos::Tokens>( ask_eos, bid.quantity );
currency::Tokens fill_amount_currency;
EosTokens fill_amount_eos = min<eos::Tokens>( ask_eos, bid.quantity );
CurrencyTokens fill_amount_currency;
if( fill_amount_eos == ask_eos ) { /// complete fill of ask
fill_amount_currency = ask.quantity;
......@@ -158,7 +158,7 @@ void apply_exchange_buy( BuyOrder order ) {
print( "lowest ask <= bid.price\n" );
match( bid, buyer_account, lowest_ask, seller_account );
if( lowest_ask.quantity == currency::Tokens(0) ) {
if( lowest_ask.quantity == CurrencyTokens(0) ) {
save( seller_account );
save( buyer_account );
Asks::remove( lowest_ask );
......@@ -188,7 +188,7 @@ void apply_exchange_sell( SellOrder order ) {
Ask& ask = order;
requireAuth( ask.seller.name );
assert( ask.quantity > currency::Tokens(0), "invalid quantity" );
assert( ask.quantity > CurrencyTokens(0), "invalid quantity" );
assert( ask.expiration > now(), "order expired" );
print( "\n\n", Name(ask.seller.name), " created sell for ", order.quantity,
......@@ -216,7 +216,7 @@ void apply_exchange_sell( SellOrder order ) {
while( highest_bid.price >= ask.price ) {
match( highest_bid, buyer_account, ask, seller_account );
if( highest_bid.quantity == eos::Tokens(0) ) {
if( highest_bid.quantity == EosTokens(0) ) {
save( seller_account );
save( buyer_account );
Bids::remove( highest_bid );
......
......@@ -2,12 +2,15 @@
namespace exchange {
using currency::CurrencyTokens;
using EosTokens = eos::Tokens;
struct OrderID {
AccountName name = 0;
uint64_t number = 0;
};
typedef eos::price<eos::Tokens,currency::Tokens> Price;
typedef eos::price<EosTokens,CurrencyTokens> Price;
struct Bid {
OrderID buyer;
......@@ -17,28 +20,24 @@ namespace exchange {
};
struct Ask {
OrderID seller;
Price price;
currency::Tokens quantity;
Time expiration;
OrderID seller;
Price price;
CurrencyTokens quantity;
Time expiration;
};
struct Account {
Account( AccountName o = AccountName() ):owner(o){}
AccountName owner;
eos::Tokens eos_balance;
currency::Tokens currency_balance;
EosTokens eos_balance;
CurrencyTokens currency_balance;
uint32_t open_orders = 0;
bool isEmpty()const { return ! ( bool(eos_balance) | bool(currency_balance) | open_orders); }
};
inline Account getAccount( AccountName owner ) {
Account account(owner);
Db::get( N(exchange), N(exchange), N(account), owner, account );
return account;
}
using Accounts = Table<N(exchange),N(exchange),N(account),Account,uint64_t>;
TABLE2(Bids,exchange,exchange,bids,Bid,BidsById,OrderID,BidsByPrice,Price);
TABLE2(Asks,exchange,exchange,bids,Ask,AsksById,OrderID,AsksByPrice,Price);
......@@ -46,5 +45,12 @@ namespace exchange {
struct BuyOrder : public Bid { uint8_t fill_or_kill = false; };
struct SellOrder : public Ask { uint8_t fill_or_kill = false; };
inline Account getAccount( AccountName owner ) {
Account account(owner);
Accounts::get( account );
return account;
}
}
......@@ -145,32 +145,35 @@ DEFINE_INTRINSIC_FUNCTION1(env,requireScope,requireScope,none,i64,scope) {
wasm_interface::get().current_validate_context->require_scope( scope );
}
DEFINE_INTRINSIC_FUNCTION5(env,store_i64,store_i64,i32,i64,scope,i64,table,i64,key,i32,valueptr,i32,valuelen)
DEFINE_INTRINSIC_FUNCTION4(env,store_i64,store_i64,i32,i64,scope,i64,table,i32,valueptr,i32,valuelen)
{
auto& wasm = wasm_interface::get();
FC_ASSERT( wasm.current_apply_context, "no apply context found" );
FC_ASSERT( valuelen >= sizeof(uint64_t) );
auto mem = wasm.current_memory;
char* value = memoryArrayPtr<char>( mem, valueptr, valuelen);
uint64_t* key = reinterpret_cast<uint64_t*>(value);
//idump((Name(scope))(Name(code))(Name(table))(Name(key))(valuelen) );
return wasm.current_apply_context->store_i64( scope, table, key, value, valuelen );
return wasm.current_apply_context->store_i64( scope, table, *key, value, valuelen );
}
DEFINE_INTRINSIC_FUNCTION6(env,load_i64,load_i64,i32,i64,scope,i64,code,i64,table,i64,key,i32,valueptr,i32,valuelen)
DEFINE_INTRINSIC_FUNCTION5(env,load_i64,load_i64,i32,i64,scope,i64,code,i64,table,i32,valueptr,i32,valuelen)
{
//idump((Name(scope))(Name(code))(Name(table))(Name(key))(valuelen) );
FC_ASSERT( valuelen >= 0 );
FC_ASSERT( valuelen >= sizeof(uint64_t) );
auto& wasm = wasm_interface::get();
FC_ASSERT( wasm.current_validate_context, "no apply context found" );
auto mem = wasm.current_memory;
char* value = memoryArrayPtr<char>( mem, valueptr, valuelen);
uint64_t* key = reinterpret_cast<uint64_t*>(value);
return wasm.current_validate_context->load_i64( scope, code, table, key, value, valuelen );
return wasm.current_validate_context->load_i64( scope, code, table, *key, value, valuelen );
}
DEFINE_INTRINSIC_FUNCTION3(env,remove_i64,remove_i64,i32,i64,scope,i64,table,i64,key) {
......@@ -182,41 +185,20 @@ DEFINE_INTRINSIC_FUNCTION3(env,remove_i64,remove_i64,i32,i64,scope,i64,table,i64
DEFINE_INTRINSIC_FUNCTION3(env,memcpy,memcpy,i32,i32,dstp,i32,srcp,i32,len) {
auto& wasm = wasm_interface::get();
auto mem = wasm.current_memory;
char* dst = &memoryRef<char>( mem, dstp);
const char* src = &memoryRef<const char>( mem, srcp );
//char* dst_end = &memoryRef<char>( mem, dstp+uint32_t(len));
const char* src_end = &memoryRef<const char>( mem, srcp+uint32_t(len) );
char* dst = memoryArrayPtr<char>( mem, dstp, len);
const char* src = memoryArrayPtr<const char>( mem, srcp, len );
FC_ASSERT( len > 0 );
#warning TODO: wasm memcpy has undefined behavior if memory ranges overlap
/*
if( dst > src )
FC_ASSERT( dst < src_end && src < dst_end, "overlap of memory range is undefined", ("d",dstp)("s",srcp)("l",len) );
FC_ASSERT( dst >= (src + len), "overlap of memory range is undefined", ("d",dstp)("s",srcp)("l",len) );
else
FC_ASSERT( src < dst_end && dst < src_end, "overlap of memory range is undefined", ("d",dstp)("s",srcp)("l",len) );
*/
FC_ASSERT( src >= (dst + len), "overlap of memory range is undefined", ("d",dstp)("s",srcp)("l",len) );
memcpy( dst, src, uint32_t(len) );
return dstp;
}
DEFINE_INTRINSIC_FUNCTION2(env,Varint_unpack,Varint_unpack,none,i32,streamptr,i32,valueptr) {
auto& wasm = wasm_interface::get();
auto mem = wasm.current_memory;
uint32_t* stream = memoryArrayPtr<uint32_t>( mem, streamptr, 3 );
const char* pos = &memoryRef<const char>( mem, stream[1] );
const char* end = &memoryRef<const char>( mem, stream[2] );
uint32_t& value = memoryRef<uint32_t>( mem, valueptr );
fc::unsigned_int vi;
fc::datastream<const char*> ds(pos,end-pos);
fc::raw::unpack( ds, vi );
value = vi.value;
stream[1] += ds.pos() - pos;
}
DEFINE_INTRINSIC_FUNCTION2(env,send,send,i32,i32,trx_buffer, i32,trx_buffer_size ) {
auto& wasm = wasm_interface::get();
auto mem = wasm.current_memory;
......@@ -304,22 +286,10 @@ DEFINE_INTRINSIC_FUNCTION1(env,prints,prints,none,i32,charptr) {
const char* str = &memoryRef<const char>( mem, charptr );
std::cerr << std::string( str, strlen(str) );
std::cerr << std::string( str, strnlen(str, wasm.current_state->mem_end-charptr) );
}
DEFINE_INTRINSIC_FUNCTION1(env,free,free,none,i32,ptr) {
}
DEFINE_INTRINSIC_FUNCTION1(env,toUpper,toUpper,none,i32,charptr) {
std::cerr << "TO UPPER CALLED\n";// << charptr << "\n";
// std::cerr << "moduleInstance: " << moduleInstance << "\n";
// /*U8* base = */Runtime::getMemoryBaseAddress( Runtime::getDefaultMemory(moduleInstance) );
//std::cerr << "Base Address: " << (int64_t)base;
//char* c = (char*)(base + charptr);
char& c = Runtime::memoryRef<char>( Runtime::getDefaultMemory(wasm_interface::get().current_module), charptr );
// std::cerr << "char: " << c <<"\n";
// if( c > 'Z' ) c -= 32;
// return 0;
}
wasm_interface& wasm_interface::get() {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册