Java tutorial
package com.brq.wallet.external.glidera.api; import android.net.Uri; import android.support.annotation.NonNull; import com.brq.wallet.MbwManager; import com.brq.wallet.api.retrofit.JacksonConverter; import com.brq.wallet.external.NullBodyAwareOkClient; import com.brq.wallet.external.glidera.api.request.AddPhoneRequest; import com.brq.wallet.external.glidera.api.request.BuyPriceRequest; import com.brq.wallet.external.glidera.api.request.BuyRequest; import com.brq.wallet.external.glidera.api.request.ConfirmPhoneRequest; import com.brq.wallet.external.glidera.api.request.SellPriceRequest; import com.brq.wallet.external.glidera.api.request.SellRequest; import com.brq.wallet.external.glidera.api.request.UpdateEmailRequest; import com.brq.wallet.external.glidera.api.request.VerifyPictureIdRequest; import com.brq.wallet.external.glidera.api.response.BuyPriceResponse; import com.brq.wallet.external.glidera.api.response.BuyResponse; import com.brq.wallet.external.glidera.api.response.GetPersonalInfoResponse; import com.brq.wallet.external.glidera.api.response.GetPhoneResponse; import com.brq.wallet.external.glidera.api.response.GlideraResponse; import com.brq.wallet.external.glidera.api.response.OAuth1Response; import com.brq.wallet.external.glidera.api.response.SellAddressResponse; import com.brq.wallet.external.glidera.api.response.SellPriceResponse; import com.brq.wallet.external.glidera.api.response.SellResponse; import com.brq.wallet.external.glidera.api.response.SetPersonalInfoResponse; import com.brq.wallet.external.glidera.api.response.StatusResponse; import com.brq.wallet.external.glidera.api.response.TransactionLimitsResponse; import com.brq.wallet.external.glidera.api.response.TransactionResponse; import com.brq.wallet.external.glidera.api.response.TransactionsResponse; import com.brq.wallet.external.glidera.api.response.TwoFactorResponse; import com.brq.wallet.external.glidera.api.response.VerifyPictureIdResponse; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.api.client.repackaged.com.google.common.base.Preconditions; import com.google.common.base.Charsets; import com.mrd.bitlib.crypto.Hmac; import com.mrd.bitlib.crypto.InMemoryPrivateKey; import com.mrd.bitlib.model.Address; import com.mrd.bitlib.model.NetworkParameters; import com.brq.wallet.external.glidera.api.request.SetPersonalInfoRequest; import com.brq.wallet.external.glidera.api.response.GlideraError; import com.mycelium.wapi.api.WapiJsonModule; import com.squareup.okhttp.Interceptor; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import org.spongycastle.util.encoders.Hex; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import okio.Buffer; import retrofit.RequestInterceptor; import retrofit.RestAdapter; import retrofit.RetrofitError; import retrofit.android.AndroidLog; import rx.Observable; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; import rx.schedulers.Schedulers; /*** * Olp */ public class GlideraService { private final static String GLIDERA_SERVICE = "glideraService"; /* Mainnet credentials */ private final static String MAINNET_CLIENT_ID = "265051094d50795bd43fc2696630b4f9"; private final static String MAINNET_URL = "https://www.glidera.io"; /* Testnet Credentials */ private final static String TESTNET_CLIENT_ID = "f9ccf1184cc574064eacd50e7ac6f8c8"; private final static String TESTNET_URL = "https://sandbox.glidera.io"; private final static String API_VERSION = "v1"; private final static String HEADER_CLIENT_ID = "X-CLIENT-ID"; private final static String HEADER_ACCESS_KEY = "X-ACCESS-KEY"; private final static String HEADER_ACCESS_NONCE = "X-ACCESS-NONCE"; private final static String HEADER_ACCESS_SIGNATURE = "X-ACCESS-SIGNATURE"; private final String clientId; private final String baseUrl; private final GlideraApi glideraApi; private final NetworkParameters networkParameters; private final Nonce _nonce = new Nonce(); private OAuth1Response _oAuth1Response; private volatile Observable<OAuth1Response> oAuth1ResponseObservable = null; private InMemoryPrivateKey bitidKey; private GlideraService(@NonNull final NetworkParameters networkParameters) { Preconditions.checkNotNull(networkParameters); this.networkParameters = networkParameters; this.baseUrl = getBaseUrl(networkParameters); if (networkParameters.isTestnet()) { clientId = TESTNET_CLIENT_ID; } else { clientId = MAINNET_CLIENT_ID; } /** * The Sha256 HMAC hash of the message. Use the secret matching the access_key to hash the message. * The message is the concatenation of the X-ACCESS-NONCE + URI of the request + message body JSON. * The final X-ACCESS-SIGNATURE is the HmacSha256 of the UTF-8 encoding of the message as a Hex encoded string */ final Interceptor apiCredentialInterceptor = new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); if (_oAuth1Response != null) { Request.Builder requestBuilder = request.newBuilder(); synchronized (_nonce) { final String nonce = _nonce.getNonceString(); final String uri = request.urlString(); String message = nonce + uri; if (request.body() != null && request.body().contentLength() > 0) { Buffer bodyBuffer = new Buffer(); request.body().writeTo(bodyBuffer); byte[] bodyBytes = bodyBuffer.readByteArray(); String body = new String(bodyBytes, Charsets.UTF_8); message += body; } final byte[] messageBytes = message.getBytes(Charsets.UTF_8); final byte[] secretBytes = _oAuth1Response.getSecret().getBytes(Charsets.UTF_8); final byte[] signatureBytes = Hmac.hmacSha256(secretBytes, messageBytes); ByteArrayOutputStream stream = new ByteArrayOutputStream(); Hex.encode(signatureBytes, stream); final String signature = stream.toString(); request = requestBuilder.header(HEADER_ACCESS_KEY, _oAuth1Response.getAccess_key()) .header(HEADER_ACCESS_NONCE, nonce).header(HEADER_ACCESS_SIGNATURE, signature) .build(); } } return chain.proceed(request); } }; OkHttpClient client = new OkHttpClient(); client.setConnectTimeout(15000, TimeUnit.MILLISECONDS); client.setReadTimeout(15000, TimeUnit.MILLISECONDS); client.networkInterceptors().add(apiCredentialInterceptor); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); //objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.); objectMapper.registerModule(new WapiJsonModule()); /* We should always add client_id to the header */ RequestInterceptor requestInterceptor = new RequestInterceptor() { @Override public void intercept(RequestFacade requestFacade) { requestFacade.addHeader(HEADER_CLIENT_ID, clientId); } }; /* Create the json adapter */ RestAdapter adapter = new RestAdapter.Builder().setEndpoint(baseUrl + "/api/" + API_VERSION + "/") //.setLogLevel(RestAdapter.LogLevel.FULL) .setLogLevel(RestAdapter.LogLevel.BASIC).setLog(new AndroidLog("Glidera")) .setConverter(new JacksonConverter(objectMapper)).setClient(new NullBodyAwareOkClient(client)) .setRequestInterceptor(requestInterceptor).build(); glideraApi = adapter.create(GlideraApi.class); } /* Getters */ public static GlideraService getInstance() { final MbwManager mbwManager = MbwManager.getInstance(null); try { return (GlideraService) mbwManager.getBackgroundObjectsCache().get(GLIDERA_SERVICE, new Callable<Object>() { @Override public Object call() throws Exception { return new GlideraService(mbwManager.getNetwork()); } }); } catch (ExecutionException executionException) { throw new RuntimeException(executionException); } } public static String getBaseUrl(NetworkParameters networkParameters) { if (networkParameters.isTestnet()) { return TESTNET_URL; } else { return MAINNET_URL; } } public static GlideraError convertRetrofitException(Throwable throwable) { if (throwable instanceof RetrofitError) { return (GlideraError) ((RetrofitError) throwable).getBodyAs(GlideraError.class); } else { return null; } } // returns an Observable that emits next, when both observables have emitted their next element private static <T1, T2, R> Observable<R> flatZip(Observable<? extends Address> o1, Observable<? extends String> o2, final Func2<Address, String, Observable<R>> zipFunction) { return Observable.merge(Observable.zip(o1, o2, zipFunction)); } private synchronized InMemoryPrivateKey getBitidKey() { if (bitidKey == null) { MbwManager manager = MbwManager.getInstance(null); bitidKey = manager.getBitIdKeyForWebsite(baseUrl + "/api/v1/authentication/bitid"); } return bitidKey; } public GlideraApi getApi() { return glideraApi; } public String getBitidRegistrationUrl() { final String nonce = _nonce.getNonceString(); final String uri = baseUrl + "/bitid/auth"; final String bitidUri = Uri.parse(uri + "?x=" + nonce).buildUpon().scheme("bitid").toString(); final String signature = getBitidKey().signMessage(bitidUri).getBase64Signature(); return Uri.parse(uri).buildUpon().appendQueryParameter("client_id", clientId) .appendQueryParameter("bitid_address", getBitidKey().getPublicKey().toAddress(networkParameters).toString()) .appendQueryParameter("bitid_uri", bitidUri).appendQueryParameter("bitid_signature", signature) .appendQueryParameter("redirect_uri", "mycelium://glideraRegistration") .appendQueryParameter("state", nonce).build().toString(); } public String getSetupUrl() { return getDeepLink("/user/setup"); } private String getDeepLink(@NonNull String path) { Preconditions.checkArgument(path.startsWith("/")); final String nonce = _nonce.getNonceString(); Uri uri = Uri.parse(baseUrl + path).buildUpon().appendQueryParameter("client_id", clientId) .appendQueryParameter("bitid_address", getBitidKey().getPublicKey().toAddress(networkParameters).toString()) .appendQueryParameter("nonce", nonce).build(); final String signature = getBitidKey().signMessage(uri.toString()).getBase64Signature(); return uri.buildUpon().appendQueryParameter("bitid_signature", signature).toString(); } /** * Once the user has successfully connected with BitID, the client can request OAuth 1 credentials to use for making futher API calls * . The credentials returned have all the permissions. * * @return OAuth1 Credentials */ public synchronized Observable<OAuth1Response> oauth1Create() { if (oAuth1ResponseObservable == null) { final Observable<Address> addressObservable = Observable.create(new Observable.OnSubscribe<Address>() { @Override public void call(Subscriber<? super Address> subscriber) { subscriber.onNext(getBitidKey().getPublicKey().toAddress(networkParameters)); subscriber.onCompleted(); } }).subscribeOn(Schedulers.computation()); final String nonce = _nonce.getNonceString(); final String uri = baseUrl + "/api/" + API_VERSION + "/authentication/oauth1/create?x=" + nonce; final Observable<String> signatureObservable = Observable.create(new Observable.OnSubscribe<String>() { @Override public void call(Subscriber<? super String> subscriber) { final String signature = getBitidKey().signMessage(uri).getBase64Signature(); subscriber.onNext(signature); subscriber.onCompleted(); } }).subscribeOn(Schedulers.computation()); oAuth1ResponseObservable = flatZip(addressObservable, signatureObservable, new Func2<Address, String, Observable<OAuth1Response>>() { @Override public Observable<OAuth1Response> call(Address address, String signature) { return glideraApi.oAuth1Create(address.toString(), uri, signature); } }).observeOn(Schedulers.newThread()).doOnError(new Action1<Throwable>() { @Override public void call(Throwable throwable) { _oAuth1Response = null; oAuth1ResponseObservable = null; } }).map(new Func1<OAuth1Response, OAuth1Response>() { @Override public OAuth1Response call(OAuth1Response oAuth1Response) { _oAuth1Response = oAuth1Response; return oAuth1Response; } }).cache(); } return oAuth1ResponseObservable; } /** * @return Return a user's email address. */ public Observable<GlideraResponse> getEmail() { return new ApiCaller<GlideraResponse>() { @Override Observable<GlideraResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().getEmail(); } }.call(); } /** * Update user's email address. An email with a verification link is sent to the new email address. Until the new email is verified * the user will continue to use the previous email address. * <p> * Note: This API call is only available to BitID/OAuth 1 clients. * * @param updateEmailRequest Request containing updated email information * @return Returns error details if present */ public Observable<GlideraResponse> updateEmail(final UpdateEmailRequest updateEmailRequest, final String twoFACode) { return new ApiCaller<GlideraResponse>() { @Override Observable<GlideraResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().updateEmail(updateEmailRequest, twoFACode); } }.call(); } /** * Request the verification email to be resent so user can verify their email address. Returns an error if the email address is * already verified. * * @return Returns error details if present */ public Observable<GlideraResponse> resendVerificationEmail() { return new ApiCaller<GlideraResponse>() { @Override Observable<GlideraResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().resendVerificationEmail(); } }.call(); } /** * Personal information includes the user's name, address, and status. The * status explains if the user can transact and has a valid bank account setup. * * @return Return a user's personally identifiable information. */ public Observable<GetPersonalInfoResponse> getPersonalInfo() { return new ApiCaller<GetPersonalInfoResponse>() { @Override Observable<GetPersonalInfoResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().getPersonalInfo(); } }.call(); } /** * Sets a user's personal info and initiates KYC identification. * <p> * Note: The user's phone number must be set before basic info can be updated. * * @param setPersonalInfoRequest Request object containing updated user personal information. * @return It returns the user's status. The status explains if the user can transact and has a valid bank account setup. */ public Observable<SetPersonalInfoResponse> setPersonalInfo( final SetPersonalInfoRequest setPersonalInfoRequest) { return new ApiCaller<SetPersonalInfoResponse>() { @Override Observable<SetPersonalInfoResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().setPersonalInfo(setPersonalInfoRequest); } }.call(); } /** * Pass in the padded base64 of a government issued identity document (drivers license, state ID, or passport) for verification. * <p> * Note: The user's personal info must be set before basic info can be updated. * * @param verifyPictureIdRequest Request containing picture id data * @return Returns the document's status. */ public Observable<VerifyPictureIdResponse> verifyPictureId( final VerifyPictureIdRequest verifyPictureIdRequest) { return new ApiCaller<VerifyPictureIdResponse>() { @Override Observable<VerifyPictureIdResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().verifyPictureId(verifyPictureIdRequest); } }.call(); } /** * A user must successfully complete setup for each item in the response to be allowed to transact ( buy / sell). * * @return Returns a user's status. */ public Observable<StatusResponse> status() { return new ApiCaller<StatusResponse>() { @Override Observable<StatusResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().status(); } }.call(); } /** * Regulations require vendors to limit the amount transacted based upon the risk of the * person performing the transaction. There are limits per transaction as well as daily and monthly limits. Limits increase as the * person passes more KYC (Know Your Customer) steps to better prove their identity. Transactions submitted in excess of the users * remaining limit will cause an error. * * @return Returns the user's buy and sell limits. */ public Observable<TransactionLimitsResponse> transactionLimits() { return new ApiCaller<TransactionLimitsResponse>() { @Override Observable<TransactionLimitsResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().transactionLimits(); } }.call(); } /** * Adds a phone number for the user. A verification code is sent to the phone number which must be confirmed to complete this step. * After calling this function, the wallet should call confirm phone number. To change phone numbers, first delete the user's phone * number and then add the new number. * <p> * Note: The user email must be confirmed before adding a phone number. An email with a confirmation link is sent after successfully * registering the user. * * @param addPhoneRequest Request containing updated phone information * @return Returns error details if present */ public Observable<GlideraResponse> addPhone(final AddPhoneRequest addPhoneRequest) { return new ApiCaller<GlideraResponse>() { @Override Observable<GlideraResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().addPhone(addPhoneRequest); } }.call(); } /** * After adding a phone number, users must confirm access by providing the verification codes sent to the new number. * * @param confirmPhoneRequest Request containing phone confirmation information. * @return Returns error details if present */ public Observable<GlideraResponse> confirmPhone(final ConfirmPhoneRequest confirmPhoneRequest) { return new ApiCaller<GlideraResponse>() { @Override Observable<GlideraResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().confirmPhone(confirmPhoneRequest); } }.call(); } /** * Deletes the user's phone number. User will not be able to transact or update any information until new phone number is added and * verified. * * @return Returns error details if present */ public Observable<GlideraResponse> deletePhone(final String twoFACode) { return new ApiCaller<GlideraResponse>() { @Override Observable<GlideraResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().deletePhone(twoFACode); } }.call(); } /** * Phone number will be blank if added but not confirmed. * * @return Returns the user's phone number. */ public Observable<GetPhoneResponse> getPhone() { return new ApiCaller<GetPhoneResponse>() { @Override Observable<GetPhoneResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().getPhone(); } }.call(); } /** * Price quotes can be passed into the buy api call or be for informational purposes only. * Quotes expire after one minute. Quotes are in the currency of the user's country. Quotes will vary based upon the amount of * bitcoin or fiat specified for purchase. * * @param buyPriceRequest Request containing buy price information * @return Return the current buy price from Glidera. */ public Observable<BuyPriceResponse> buyPrice(final BuyPriceRequest buyPriceRequest) { return new ApiCaller<BuyPriceResponse>() { @Override Observable<BuyPriceResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().buyPrice(buyPriceRequest); } }.call(); } /** * Buy Bitcoin and send it to the destinationAddress. The fiat being spent on the purchase is electronically debited from the user's * verified bank account (by ACH, EFT, SEPA, etc). The market price can be used or a current Glidera price quote from a previous Buy * Prices service call can be used. This service requires a Two Factor Authentication code by previously calling Get Two Factor Code * service. * * @param buyRequest Request containing buy information. * @return Returns a buy response containing transaction details and estimated delivery date. */ public Observable<BuyResponse> buy(final BuyRequest buyRequest, final String twoFACode) { return new ApiCaller<BuyResponse>() { @Override Observable<BuyResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().buy(buyRequest, twoFACode); } }.call(); } /** * Send Bitcoin to this address when using the Sell service. * * @return Return a Glidera sell address. */ public Observable<SellAddressResponse> sellAddress() { return new ApiCaller<SellAddressResponse>() { @Override Observable<SellAddressResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().sellAddress(); } }.call(); } /** * Price quotes can be passed in to the sell api call or be for informational purposes only. * Quotes expire after one minute. Quotes are in the currency of the user's country. Sell prices will vary based upon quantity. * * @param sellPriceRequest Request containing sell information. * @return Return a sell price quote from Glidera. */ public Observable<SellPriceResponse> sellPrice(final SellPriceRequest sellPriceRequest) { return new ApiCaller<SellPriceResponse>() { @Override Observable<SellPriceResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().sellPrice(sellPriceRequest); } }.call(); } /** * Sell Bitcoin by sending in a signed raw transaction. Glidera will broadcast successful transactions. One of the outputs of this * signed transaction must be a Glidera sell address. Sell addresses are created using the Create Sell Address service. The current * market price can be used or a Glidera price quote from the Sell Prices service can be used. If a failure occurs, Glidera will NOT * broadcast the transaction and the client can double spend the inputs if it desires. * * @param sellRequest Request containg sell information. * @return Returns sell object containing transaction information as well as estimated delivery date. */ public Observable<SellResponse> sell(final SellRequest sellRequest) { return new ApiCaller<SellResponse>() { @Override Observable<SellResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().sell(sellRequest); } }.call(); } /** * @return Return information about all previously performed Buy or Sell transaction. */ public Observable<TransactionsResponse> transaction() { return new ApiCaller<TransactionsResponse>() { @Override Observable<TransactionsResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().transaction(); } }.call(); } /** * @param transactionUuid The uuid of the transaction to return. The transactionUUID in the URL matches the transactionUuid for the * transaction on the Glidera website. This UUID is also returned when using the Buy and Sell services. * @return Return information about previously performed Buy or Sell transaction. */ public Observable<TransactionResponse> transaction(final UUID transactionUuid) { return new ApiCaller<TransactionResponse>() { @Override Observable<TransactionResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().transaction(transactionUuid); } }.call(); } /** * There are a number of endpoints that require 2FA codes if two-factor authentication is enabled by the user. This includes buy, * sell, etc. If the user is configured to recieve SMS for their two factor verification this API call causes Glidera to send an SMS * message to the user's phone. The 2FA code must be passed in on the next service call in the X-2FA-CODE header. Use this service * right before calling a Two Factor Required API call. * <p> * Some users may be configured to use an authenticator app (Authy or Google Authenticator), and an SMS message will NOT be sent. In * either case, the wallet application will need to prompt the user to enter a proper 2FA code to successfully pass the subsequent * service call. * <p> * Users may also have enabled PIN based two-factor authentication. In this case the application must prompt the user for a PIN, and * no SMS meesage will be sent. This API call will return the appropriate mode for the user's two factor authentication. * * @return Returns type of two factor, as well as confirmation two factor was sent if appropriate. */ public Observable<TwoFactorResponse> getTwoFactor() { return new ApiCaller<TwoFactorResponse>() { @Override Observable<TwoFactorResponse> apiCall(OAuth1Response oAuth1Response) { return getApi().getTwoFactor(); } }.call(); } /** * Perform api call, will first get fresh api credentials if they are needed. If an error occures while performing call, current api * credentials will be cleared * * @param <T> The glidera response we intend to recieve */ private abstract class ApiCaller<T> { private final Observable<OAuth1Response> apiCredentialResponseObservable; public ApiCaller() { apiCredentialResponseObservable = oauth1Create(); } public Observable<T> call() { Observable<T> responseObservable; if (_oAuth1Response == null) { responseObservable = apiCredentialResponseObservable .flatMap(new Func1<OAuth1Response, Observable<T>>() { @Override public Observable<T> call(OAuth1Response apiSecretToken) { return apiCall(apiSecretToken).observeOn(Schedulers.io()); } }); } else { responseObservable = apiCall(_oAuth1Response).doOnError(new Action1<Throwable>() { @Override public void call(Throwable throwable) { if (throwable instanceof RetrofitError) { GlideraError error = convertRetrofitException(throwable); if (error != null) { //Log.e("Glidera", error.toString()); if (error.getCode() == GlideraError.ERROR_INVALID_AUTH1 || error.getCode() == GlideraError.ERROR_INVALID_AUTH2) { _oAuth1Response = null; } } } } }).retry(new Func2<Integer, Throwable, Boolean>() { @Override public Boolean call(Integer integer, Throwable throwable) { /* Retry up to three times */ if (integer > 2) { return false; } GlideraError error = convertRetrofitException(throwable); if (error != null) { /* If nonce is too little, null the nonce and try again */ if (error.getCode() == GlideraError.ERROR_INVALID_NONCE) { _nonce.resetNonce(); return true; } } return false; } }).map(new Func1<T, T>() { @Override public T call(T t) { return t; } }).observeOn(Schedulers.newThread()); } return responseObservable.map(new Func1<T, T>() { @Override public T call(T t) { //Log.d("Glidera", t.toString()); return t; } }).observeOn(AndroidSchedulers.mainThread()); } abstract Observable<T> apiCall(OAuth1Response apiSecretToken); } }