rules_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. // Copyright 2018 The go-ethereum Authors
  2. // This file is part of go-ethereum.
  3. //
  4. // go-ethereum is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // go-ethereum is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
  16. //
  17. package rules
  18. import (
  19. "fmt"
  20. "math/big"
  21. "strings"
  22. "testing"
  23. "github.com/ethereum/go-ethereum/accounts"
  24. "github.com/ethereum/go-ethereum/common"
  25. "github.com/ethereum/go-ethereum/common/hexutil"
  26. "github.com/ethereum/go-ethereum/core/types"
  27. "github.com/ethereum/go-ethereum/internal/ethapi"
  28. "github.com/ethereum/go-ethereum/signer/core"
  29. "github.com/ethereum/go-ethereum/signer/storage"
  30. )
  31. const JS = `
  32. /**
  33. This is an example implementation of a Javascript rule file.
  34. When the signer receives a request over the external API, the corresponding method is evaluated.
  35. Three things can happen:
  36. 1. The method returns "Approve". This means the operation is permitted.
  37. 2. The method returns "Reject". This means the operation is rejected.
  38. 3. Anything else; other return values [*], method not implemented or exception occurred during processing. This means
  39. that the operation will continue to manual processing, via the regular UI method chosen by the user.
  40. [*] Note: Future version of the ruleset may use more complex json-based returnvalues, making it possible to not
  41. only respond Approve/Reject/Manual, but also modify responses. For example, choose to list only one, but not all
  42. accounts in a list-request. The points above will continue to hold for non-json based responses ("Approve"/"Reject").
  43. **/
  44. function ApproveListing(request){
  45. console.log("In js approve listing");
  46. console.log(request.accounts[3].Address)
  47. console.log(request.meta.Remote)
  48. return "Approve"
  49. }
  50. function ApproveTx(request){
  51. console.log("test");
  52. console.log("from");
  53. return "Reject";
  54. }
  55. function test(thing){
  56. console.log(thing.String())
  57. }
  58. `
  59. func mixAddr(a string) (*common.MixedcaseAddress, error) {
  60. return common.NewMixedcaseAddressFromString(a)
  61. }
  62. type alwaysDenyUI struct{}
  63. func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) {
  64. }
  65. func (alwaysDenyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
  66. return core.SignTxResponse{Transaction: request.Transaction, Approved: false, Password: ""}, nil
  67. }
  68. func (alwaysDenyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
  69. return core.SignDataResponse{Approved: false, Password: ""}, nil
  70. }
  71. func (alwaysDenyUI) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) {
  72. return core.ExportResponse{Approved: false}, nil
  73. }
  74. func (alwaysDenyUI) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) {
  75. return core.ImportResponse{Approved: false, OldPassword: "", NewPassword: ""}, nil
  76. }
  77. func (alwaysDenyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
  78. return core.ListResponse{Accounts: nil}, nil
  79. }
  80. func (alwaysDenyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
  81. return core.NewAccountResponse{Approved: false, Password: ""}, nil
  82. }
  83. func (alwaysDenyUI) ShowError(message string) {
  84. panic("implement me")
  85. }
  86. func (alwaysDenyUI) ShowInfo(message string) {
  87. panic("implement me")
  88. }
  89. func (alwaysDenyUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
  90. panic("implement me")
  91. }
  92. func initRuleEngine(js string) (*rulesetUI, error) {
  93. r, err := NewRuleEvaluator(&alwaysDenyUI{}, storage.NewEphemeralStorage(), storage.NewEphemeralStorage())
  94. if err != nil {
  95. return nil, fmt.Errorf("failed to create js engine: %v", err)
  96. }
  97. if err = r.Init(js); err != nil {
  98. return nil, fmt.Errorf("failed to load bootstrap js: %v", err)
  99. }
  100. return r, nil
  101. }
  102. func TestListRequest(t *testing.T) {
  103. accs := make([]core.Account, 5)
  104. for i := range accs {
  105. addr := fmt.Sprintf("000000000000000000000000000000000000000%x", i)
  106. acc := core.Account{
  107. Address: common.BytesToAddress(common.Hex2Bytes(addr)),
  108. URL: accounts.URL{Scheme: "test", Path: fmt.Sprintf("acc-%d", i)},
  109. }
  110. accs[i] = acc
  111. }
  112. js := `function ApproveListing(){ return "Approve" }`
  113. r, err := initRuleEngine(js)
  114. if err != nil {
  115. t.Errorf("Couldn't create evaluator %v", err)
  116. return
  117. }
  118. resp, err := r.ApproveListing(&core.ListRequest{
  119. Accounts: accs,
  120. Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
  121. })
  122. if len(resp.Accounts) != len(accs) {
  123. t.Errorf("Expected check to resolve to 'Approve'")
  124. }
  125. }
  126. func TestSignTxRequest(t *testing.T) {
  127. js := `
  128. function ApproveTx(r){
  129. console.log("transaction.from", r.transaction.from);
  130. console.log("transaction.to", r.transaction.to);
  131. console.log("transaction.value", r.transaction.value);
  132. console.log("transaction.nonce", r.transaction.nonce);
  133. if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"}
  134. if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"}
  135. }`
  136. r, err := initRuleEngine(js)
  137. if err != nil {
  138. t.Errorf("Couldn't create evaluator %v", err)
  139. return
  140. }
  141. to, err := mixAddr("000000000000000000000000000000000000dead")
  142. if err != nil {
  143. t.Error(err)
  144. return
  145. }
  146. from, err := mixAddr("0000000000000000000000000000000000001337")
  147. if err != nil {
  148. t.Error(err)
  149. return
  150. }
  151. fmt.Printf("to %v", to.Address().String())
  152. resp, err := r.ApproveTx(&core.SignTxRequest{
  153. Transaction: core.SendTxArgs{
  154. From: *from,
  155. To: to},
  156. Callinfo: nil,
  157. Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
  158. })
  159. if err != nil {
  160. t.Errorf("Unexpected error %v", err)
  161. }
  162. if !resp.Approved {
  163. t.Errorf("Expected check to resolve to 'Approve'")
  164. }
  165. }
  166. type dummyUI struct {
  167. calls []string
  168. }
  169. func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
  170. d.calls = append(d.calls, "ApproveTx")
  171. return core.SignTxResponse{}, core.ErrRequestDenied
  172. }
  173. func (d *dummyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
  174. d.calls = append(d.calls, "ApproveSignData")
  175. return core.SignDataResponse{}, core.ErrRequestDenied
  176. }
  177. func (d *dummyUI) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) {
  178. d.calls = append(d.calls, "ApproveExport")
  179. return core.ExportResponse{}, core.ErrRequestDenied
  180. }
  181. func (d *dummyUI) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) {
  182. d.calls = append(d.calls, "ApproveImport")
  183. return core.ImportResponse{}, core.ErrRequestDenied
  184. }
  185. func (d *dummyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
  186. d.calls = append(d.calls, "ApproveListing")
  187. return core.ListResponse{}, core.ErrRequestDenied
  188. }
  189. func (d *dummyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
  190. d.calls = append(d.calls, "ApproveNewAccount")
  191. return core.NewAccountResponse{}, core.ErrRequestDenied
  192. }
  193. func (d *dummyUI) ShowError(message string) {
  194. d.calls = append(d.calls, "ShowError")
  195. }
  196. func (d *dummyUI) ShowInfo(message string) {
  197. d.calls = append(d.calls, "ShowInfo")
  198. }
  199. func (d *dummyUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
  200. d.calls = append(d.calls, "OnApprovedTx")
  201. }
  202. func (d *dummyUI) OnSignerStartup(info core.StartupInfo) {
  203. }
  204. //TestForwarding tests that the rule-engine correctly dispatches requests to the next caller
  205. func TestForwarding(t *testing.T) {
  206. js := ""
  207. ui := &dummyUI{make([]string, 0)}
  208. jsBackend := storage.NewEphemeralStorage()
  209. credBackend := storage.NewEphemeralStorage()
  210. r, err := NewRuleEvaluator(ui, jsBackend, credBackend)
  211. if err != nil {
  212. t.Fatalf("Failed to create js engine: %v", err)
  213. }
  214. if err = r.Init(js); err != nil {
  215. t.Fatalf("Failed to load bootstrap js: %v", err)
  216. }
  217. r.ApproveSignData(nil)
  218. r.ApproveTx(nil)
  219. r.ApproveImport(nil)
  220. r.ApproveNewAccount(nil)
  221. r.ApproveListing(nil)
  222. r.ApproveExport(nil)
  223. r.ShowError("test")
  224. r.ShowInfo("test")
  225. //This one is not forwarded
  226. r.OnApprovedTx(ethapi.SignTransactionResult{})
  227. expCalls := 8
  228. if len(ui.calls) != expCalls {
  229. t.Errorf("Expected %d forwarded calls, got %d: %s", expCalls, len(ui.calls), strings.Join(ui.calls, ","))
  230. }
  231. }
  232. func TestMissingFunc(t *testing.T) {
  233. r, err := initRuleEngine(JS)
  234. if err != nil {
  235. t.Errorf("Couldn't create evaluator %v", err)
  236. return
  237. }
  238. _, err = r.execute("MissingMethod", "test")
  239. if err == nil {
  240. t.Error("Expected error")
  241. }
  242. approved, err := r.checkApproval("MissingMethod", nil, nil)
  243. if err == nil {
  244. t.Errorf("Expected missing method to yield error'")
  245. }
  246. if approved {
  247. t.Errorf("Expected missing method to cause non-approval")
  248. }
  249. fmt.Printf("Err %v", err)
  250. }
  251. func TestStorage(t *testing.T) {
  252. js := `
  253. function testStorage(){
  254. storage.Put("mykey", "myvalue")
  255. a = storage.Get("mykey")
  256. storage.Put("mykey", ["a", "list"]) // Should result in "a,list"
  257. a += storage.Get("mykey")
  258. storage.Put("mykey", {"an": "object"}) // Should result in "[object Object]"
  259. a += storage.Get("mykey")
  260. storage.Put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
  261. a += storage.Get("mykey")
  262. a += storage.Get("missingkey") //Missing keys should result in empty string
  263. storage.Put("","missing key==noop") // Can't store with 0-length key
  264. a += storage.Get("") // Should result in ''
  265. var b = new BigNumber(2)
  266. var c = new BigNumber(16)//"0xf0",16)
  267. var d = b.plus(c)
  268. console.log(d)
  269. return a
  270. }
  271. `
  272. r, err := initRuleEngine(js)
  273. if err != nil {
  274. t.Errorf("Couldn't create evaluator %v", err)
  275. return
  276. }
  277. v, err := r.execute("testStorage", nil)
  278. if err != nil {
  279. t.Errorf("Unexpected error %v", err)
  280. }
  281. retval, err := v.ToString()
  282. if err != nil {
  283. t.Errorf("Unexpected error %v", err)
  284. }
  285. exp := `myvaluea,list[object Object]{"an":"object"}`
  286. if retval != exp {
  287. t.Errorf("Unexpected data, expected '%v', got '%v'", exp, retval)
  288. }
  289. fmt.Printf("Err %v", err)
  290. }
  291. const ExampleTxWindow = `
  292. function big(str){
  293. if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
  294. return new BigNumber(str)
  295. }
  296. // Time window: 1 week
  297. var window = 1000* 3600*24*7;
  298. // Limit : 1 ether
  299. var limit = new BigNumber("1e18");
  300. function isLimitOk(transaction){
  301. var value = big(transaction.value)
  302. // Start of our window function
  303. var windowstart = new Date().getTime() - window;
  304. var txs = [];
  305. var stored = storage.Get('txs');
  306. if(stored != ""){
  307. txs = JSON.parse(stored)
  308. }
  309. // First, remove all that have passed out of the time-window
  310. var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart});
  311. console.log(txs, newtxs.length);
  312. // Secondly, aggregate the current sum
  313. sum = new BigNumber(0)
  314. sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum);
  315. console.log("ApproveTx > Sum so far", sum);
  316. console.log("ApproveTx > Requested", value.toNumber());
  317. // Would we exceed weekly limit ?
  318. return sum.plus(value).lt(limit)
  319. }
  320. function ApproveTx(r){
  321. console.log(r)
  322. console.log(typeof(r))
  323. if (isLimitOk(r.transaction)){
  324. return "Approve"
  325. }
  326. return "Nope"
  327. }
  328. /**
  329. * OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter
  330. * 'response_str' contains the return value that will be sent to the external caller.
  331. * The return value from this method is ignore - the reason for having this callback is to allow the
  332. * ruleset to keep track of approved transactions.
  333. *
  334. * When implementing rate-limited rules, this callback should be used.
  335. * If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user
  336. * then accepts the transaction, this method will be called.
  337. *
  338. * TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx.
  339. */
  340. function OnApprovedTx(resp){
  341. var value = big(resp.tx.value)
  342. var txs = []
  343. // Load stored transactions
  344. var stored = storage.Get('txs');
  345. if(stored != ""){
  346. txs = JSON.parse(stored)
  347. }
  348. // Add this to the storage
  349. txs.push({tstamp: new Date().getTime(), value: value});
  350. storage.Put("txs", JSON.stringify(txs));
  351. }
  352. `
  353. func dummyTx(value hexutil.Big) *core.SignTxRequest {
  354. to, _ := mixAddr("000000000000000000000000000000000000dead")
  355. from, _ := mixAddr("000000000000000000000000000000000000dead")
  356. n := hexutil.Uint64(3)
  357. gas := hexutil.Uint64(21000)
  358. gasPrice := hexutil.Big(*big.NewInt(2000000))
  359. return &core.SignTxRequest{
  360. Transaction: core.SendTxArgs{
  361. From: *from,
  362. To: to,
  363. Value: value,
  364. Nonce: n,
  365. GasPrice: gasPrice,
  366. Gas: gas,
  367. },
  368. Callinfo: []core.ValidationInfo{
  369. {Typ: "Warning", Message: "All your base are bellong to us"},
  370. },
  371. Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
  372. }
  373. }
  374. func dummyTxWithV(value uint64) *core.SignTxRequest {
  375. v := big.NewInt(0).SetUint64(value)
  376. h := hexutil.Big(*v)
  377. return dummyTx(h)
  378. }
  379. func dummySigned(value *big.Int) *types.Transaction {
  380. to := common.HexToAddress("000000000000000000000000000000000000dead")
  381. gas := uint64(21000)
  382. gasPrice := big.NewInt(2000000)
  383. data := make([]byte, 0)
  384. return types.NewTransaction(3, to, value, gas, gasPrice, data)
  385. }
  386. func TestLimitWindow(t *testing.T) {
  387. r, err := initRuleEngine(ExampleTxWindow)
  388. if err != nil {
  389. t.Errorf("Couldn't create evaluator %v", err)
  390. return
  391. }
  392. // 0.3 ether: 429D069189E0000 wei
  393. v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000"))
  394. h := hexutil.Big(*v)
  395. // The first three should succeed
  396. for i := 0; i < 3; i++ {
  397. unsigned := dummyTx(h)
  398. resp, err := r.ApproveTx(unsigned)
  399. if err != nil {
  400. t.Errorf("Unexpected error %v", err)
  401. }
  402. if !resp.Approved {
  403. t.Errorf("Expected check to resolve to 'Approve'")
  404. }
  405. // Create a dummy signed transaction
  406. response := ethapi.SignTransactionResult{
  407. Tx: dummySigned(v),
  408. Raw: common.Hex2Bytes("deadbeef"),
  409. }
  410. r.OnApprovedTx(response)
  411. }
  412. // Fourth should fail
  413. resp, err := r.ApproveTx(dummyTx(h))
  414. if resp.Approved {
  415. t.Errorf("Expected check to resolve to 'Reject'")
  416. }
  417. }
  418. // dontCallMe is used as a next-handler that does not want to be called - it invokes test failure
  419. type dontCallMe struct {
  420. t *testing.T
  421. }
  422. func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) {
  423. }
  424. func (d *dontCallMe) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
  425. d.t.Fatalf("Did not expect next-handler to be called")
  426. return core.SignTxResponse{}, core.ErrRequestDenied
  427. }
  428. func (d *dontCallMe) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
  429. d.t.Fatalf("Did not expect next-handler to be called")
  430. return core.SignDataResponse{}, core.ErrRequestDenied
  431. }
  432. func (d *dontCallMe) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) {
  433. d.t.Fatalf("Did not expect next-handler to be called")
  434. return core.ExportResponse{}, core.ErrRequestDenied
  435. }
  436. func (d *dontCallMe) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) {
  437. d.t.Fatalf("Did not expect next-handler to be called")
  438. return core.ImportResponse{}, core.ErrRequestDenied
  439. }
  440. func (d *dontCallMe) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
  441. d.t.Fatalf("Did not expect next-handler to be called")
  442. return core.ListResponse{}, core.ErrRequestDenied
  443. }
  444. func (d *dontCallMe) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
  445. d.t.Fatalf("Did not expect next-handler to be called")
  446. return core.NewAccountResponse{}, core.ErrRequestDenied
  447. }
  448. func (d *dontCallMe) ShowError(message string) {
  449. d.t.Fatalf("Did not expect next-handler to be called")
  450. }
  451. func (d *dontCallMe) ShowInfo(message string) {
  452. d.t.Fatalf("Did not expect next-handler to be called")
  453. }
  454. func (d *dontCallMe) OnApprovedTx(tx ethapi.SignTransactionResult) {
  455. d.t.Fatalf("Did not expect next-handler to be called")
  456. }
  457. //TestContextIsCleared tests that the rule-engine does not retain variables over several requests.
  458. // if it does, that would be bad since developers may rely on that to store data,
  459. // instead of using the disk-based data storage
  460. func TestContextIsCleared(t *testing.T) {
  461. js := `
  462. function ApproveTx(){
  463. if (typeof foobar == 'undefined') {
  464. foobar = "Approve"
  465. }
  466. console.log(foobar)
  467. if (foobar == "Approve"){
  468. foobar = "Reject"
  469. }else{
  470. foobar = "Approve"
  471. }
  472. return foobar
  473. }
  474. `
  475. ui := &dontCallMe{t}
  476. r, err := NewRuleEvaluator(ui, storage.NewEphemeralStorage(), storage.NewEphemeralStorage())
  477. if err != nil {
  478. t.Fatalf("Failed to create js engine: %v", err)
  479. }
  480. if err = r.Init(js); err != nil {
  481. t.Fatalf("Failed to load bootstrap js: %v", err)
  482. }
  483. tx := dummyTxWithV(0)
  484. r1, err := r.ApproveTx(tx)
  485. r2, err := r.ApproveTx(tx)
  486. if r1.Approved != r2.Approved {
  487. t.Errorf("Expected execution context to be cleared between executions")
  488. }
  489. }
  490. func TestSignData(t *testing.T) {
  491. js := `function ApproveListing(){
  492. return "Approve"
  493. }
  494. function ApproveSignData(r){
  495. if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
  496. {
  497. if(r.message.indexOf("bazonk") >= 0){
  498. return "Approve"
  499. }
  500. return "Reject"
  501. }
  502. // Otherwise goes to manual processing
  503. }`
  504. r, err := initRuleEngine(js)
  505. if err != nil {
  506. t.Errorf("Couldn't create evaluator %v", err)
  507. return
  508. }
  509. message := []byte("baz bazonk foo")
  510. hash, msg := core.SignHash(message)
  511. raw := hexutil.Bytes(message)
  512. addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa")
  513. fmt.Printf("address %v %v\n", addr.String(), addr.Original())
  514. resp, err := r.ApproveSignData(&core.SignDataRequest{
  515. Address: *addr,
  516. Message: msg,
  517. Hash: hash,
  518. Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
  519. Rawdata: raw,
  520. })
  521. if err != nil {
  522. t.Fatalf("Unexpected error %v", err)
  523. }
  524. if !resp.Approved {
  525. t.Fatalf("Expected approved")
  526. }
  527. }